Как стать автором
Обновить
83.81
SimbirSoft
Лидер в разработке современных ИТ-решений на заказ

Как мы разрабатывали браузерную игру: взгляд со стороны frontend-архитектора

Уровень сложностиСредний
Время на прочтение24 мин
Количество просмотров6K

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

Я Антон, руководитель Архитектурного комитета SimbirSoft, и в этой статье я расскажу о полученном опыте с точки зрения технологических особенностей реализации frontend-части Рассмотрим большое количество нестандартных элементов игрового интерфейса и общие требования и ограничения к frontend-части приложения (архитектура, model, service, store и т.д.). Поделюсь, как реализовали:

  • набор визуальных элементов приложения;

  • элементы пагинации;

  • сложный компонент на примере кнопки;

  • составной компонент на примере g-card-list;

  • анимацию.

Начало пути

Исходными данными на старте реализации стали следующие требования:

1. Удобный интерфейс пользователя

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

Что лучше? Заставить пользователя подождать десяток секунд в начале, чтобы загрузить всю основную информацию и инициализировать все объекты, или делать загрузку и инициализацию при открытии определенной страницы или вкладки? Поместить большую часть данных в локальное хранилище или всегда запрашивать эти данные с сервера? Сделать загрузку данных фоновым процессом или всегда работать только с «живыми» данными? 

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

Учитывая всё это, выделим основные требования, которые должны соблюдаться при проектировании архитектуры приложения:

  • Переходы между экранами и загрузка данных не должны занимать много времени (отзывчивость интерфейса)

  • Обновление данных должно осуществляться быстро и незаметно для пользователя

  • Источники обновления информации со стороны backend могут быть как синхронные (через стандартный REST API) — это запросы на обновление при переходе на другую страницу или совершения некоторых действий пользователя, так и асинхронные (через web-сокеты) — при динамических изменениях, например при пересчете рейтингов игроков или появление нового события, о котором нужно уведомить пользователя.

2. Большое количество нестандартных элементов игрового интерфейса

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

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

  • Общее количество различных элементов интерфейса около 40 штук

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

  • В основе большинства элементов лежит работа с SVG-графикой — отдельные части интерактивных элементов состоят из набора SVG-картинок, с которыми необходимо работать во время интерактива

  • Использование анимации для некоторых элементов интерфейса

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

  • Проанализировать все дизайн-макеты, выделить набор типовых элементов и сформулировать требования к их реализации

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

  • Создать библиотеку типовых игровых элементов интерфейса

  • Создать набор готовых анимаций

В качестве базового UI-фреймворка было решено использовать Vuetify (Version: 2.2.32) благодаря его гибкости, наличию большого числа разнообразных компонентов, а также поддержки кроссбраузерности и адаптивности. Также для реализации был выбран HTML-препроцессор PUG (Version: 3.0.0) и CSS-препроцессор Stylus (Version: 0.54.5).

3. Общие требования и ограничения к frontend-части приложения:

  • Платформа: web-приложение на основе фреймворка VueJS (Version: 2.6.11) + Nuxt.js (Version: 2.0.0, mode: SPA)

  • Корректное отображение в браузерах: Google Chrome, Mozilla FireFox, Opera последних версий

  • Использование возможностей HTML и CSS при реализации без привлечения дополнительных инструментов наподобие WebGL или Blend4Web

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

1. Разработка архитектуры приложения

Архитектура frontend-приложения, как и любая другая архитектура в IT, должна прежде всего обеспечивать следующие требования:

  • Гибкость — возможность легкого расширения функциональности

  • Универсальность — возможность выносить базовую функциональность в ядро системы для повышения эффективности повторно используемых компонентов

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

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

  • Разделение на слои — позволяет лучше разделять ответственность между функциями, а код становится проще поддерживать и сопровождать 

  • Слои очерчены не строго — слой может обращаться на нижестоящие слои и на самого себя (сервисы могут обращаться друг к другу)

  • Уход от реализации бизнес-логики в компонентах — упрощает логику, реализуя принцип разделения логики и отображения

  • Использование ООП — позволяет более просто описать отдельные предметные области и повысить процент повторного использования кода

  • Централизованное хранение данных на базе Vuex — все данные хранятся централизованно, что позволяет их обновлять из различных источников и упрощать доступ к ним из любой части приложения.

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

Рисунок 1. Модель архитектуры frontend-приложения
Рисунок 1. Модель архитектуры frontend-приложения

1.1. Модель (model)

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

Пример модели показан в Листинге 1.

1 import BaseService from './base'
2 export default class User extends BaseService {
3   constructor (data = {}) {
4    super(data)
5    this.id = data.id
6    this.firstName = data.first_name
7    this.secondName = data.second_name
8    this.avatarUrl = data.avatar_url
9    this.level = data.level
10    this.resources = data.resources || []
11    this.experience = data.experience
12  }
13
14  getFullName () {
15    return `${this.firstName} ${this.secondName}`
16  }
17 }

Листинг 1. Пример реализации модели для сущности «Пользователь»

В данном примере показаны четыре основных функции модели:

  1. Обеспечивается постоянство пространства имен внутри приложения — если на стороне backend будет принято решение изменить имя поля в теле ответа, то со стороны фронта можно ограничится только изменением модели (строки 5-11)

  2. Задаются правила именования переменных — переход с kebab-case, который используется на беке в camelCase, который применяется на фронте (строки 5-11)

  3. Осуществляется инициализация по умолчанию для массива resources — если такого поля нет в ответе с бека, то инициализируем его как пустой массив (строка 10)

  4. Функция getFullName() выполняет роль геттера, который формирует полное имя пользователя (строки 14-16)

1.2. Сервис (service)

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

  1. Сервисы общего назначения — например, сервис API содержит функции доступа к API через axios или сервис для работы с web-сокетами, кэширования и т.п.

  2. Сервисы сущностей — реализуют бизнес-логику, необходимую для функционирования сущности. Например, сервис users отвечает за работу с пользователем и содержит функции login, logout, get, getUserResources и т.п.

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

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

  • getList — функция для получения списка всех элементов сущности

  • addItem — функция добавления нового элемента

  • deleteItem — функция удаления элемента

  • updateItem — функция обновления

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

Рассмотрим сервис, который содержит бизнес-логику для сущности «Предметы». Он реализует сразу несколько различных функций:

  • getList — получает список предметов, которые есть у текущего игрока

  • addItem — добавление нового предмета: после того, как игрок купил новый предмет в магазине или получил в качестве награды, вызывается данная функция, которая добавляет полученный предмет в сумку героя

  • deleteItem — удаление предметов: их можно продавать, поэтому данная функция удаляет предмет из сумки героя

  • updateItem — функция обновления информации о предмете: вызывается после того, как прошел бой, и предмет, возможно, был поврежден

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

  • unEquip — функция разэкипировки героя: вызывается, когда герой снимает предмет экипировки и кладет его обратно в сумку.

  Исходный код сервиса Items показан в Листинге 2:

1 // Подключение моделей и сервисов
2 import BaseService from '~/services/base'
3 // Сервис для работы с API
4 import ApiService from '~/services/api'
5 // Сервис для работы со справочником слотов
6 import SlotsService from '~/services/directories/slots'
7 // Сервис работы с пользователем
8 import UserService from '~/services/user'
9 // Модели для используемых сущностей
10 import ItemModel from '~/models/items/item'
11 export default class Items extends BaseService {
12 // Получение данных с сервера и их сохранение в Vuex
13  static getList (params = {}) {
14    return ApiService.getList('items', params)
15      .then(itemsData => {
16        const Items = {
17          items: [],
18          total: 0,
19          page: 0
20        }
21        itemsData.data.forEach(itemData => {
22          // Каждый элемент "прогоняется" через модель
23          Items.items.push(new ItemModel(itemData))
24        })
25        Items.total = itemsData.meta.total
26        Items.page = itemsData.meta.current_page
27        // Сохранение полученных данных в Vuex
28        this.vuex.dispatch('setItems', Items)
29      })
30  }
31  // Добавление нового элемента данных  
32  static addItem (item) {
33    return ApiService.addItem('items', item)
34  }
35  // Удаление элемента данных
36  static deleteItem (id) {
37    return ApiService.deleteItem('items', id)
38  }
39  // Изменение информации о сущности
40  static updateItem (id, item) {
41    return ApiService.updateItem('items', id, item)
42  }
43  // Функция экипировки персонажа
44  static equip (item, equipmentSlot = null) {
45    return this.api.equip(item, equipmentSlot)
46      .then(() => {
47        UserService.getInventory()
48          .then(() => {
49            SlotsService.updateAmuletSlots()
50            SlotsService.updateElixirSlots()
51            UserService.get()
52          })
53      })
54      .catch(error => {
55        this.error(error)
56      })
57  }
58  // Функция снятия экипировки с персонажа
59  static unEquip (item) {
60    return this.api.unEquip(item)
61      .then(() => {
62        UserService.getInventory()
63          .then(() => {
64            SlotsService.updateAmuletSlots()
65            SlotsService.updateElixirSlots()
66            UserService.get()
67          })
68      })
69      .catch(error => {
70        this.error(error)
71      })
72  }

 Листинг 2. Пример реализации сервиса «Предметы (Items)»

В данном примере показаны основные принципы работы сервиса «Предметы». На что следует обратить внимание по исходному коду:

  1. Сервисы могут использовать друг друга. Это относится как к сервисам общего назначения (в данном примере это API сервис и UserService), так и к другим обычным сервисам (здесь показан пример вызова функций сервиса SlotsService в функциях equip и unEquip — строки 49-50 и 64-64)

  2. При получении данных с сервера с помощью функции getList() все полученные данные проходят через модель (строка 23). Это обеспечивает унификацию моделей и приносит все преимущества, которые описаны в разделе 1.1 «Модель»

  3. После получения данных и их типизации через модель данные сохраняются в Vuex. Логику сохранения также обеспечивает сервис (строка 28). 

1.3. Хранилище (Store)

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

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

Пример реализации набора данных и функций для сущности «Пользователь» приведен в Листингах 3, 4, 5 и 6.

1 import mutations from './mutations'
2 import actions from './actions'
3 import modules from './modules'
4 export default {
5   namespaced: true,
6   state: () => {
7     return {
8       user: null,
9       ...
10     }
11   },
12   mutations,
13   actions,
14   modules  
15 }

 Листинг 3. Файл “index.js” с описанием store

1 export const USER_SET = 'USER_SET'
2 …

Листинг 4.  Файл “mutations-types.js” с описанием типов мутаций

1 import * as types from './mutations-types'
2 export default {
3  [types.USER_SET] (state, user) {
4    state.user = user
5  }
6 …
7 }

Листинг 5.  Файл “mutations.js” с описанием мутаций

1 import * as types from './mutations-types'
2 export default {
3  setUser ({ commit }, user) {
4    return new Promise(function (resolve) {
5      commit(types.USER_SET, user)
6      resolve()
7    })
8  },
9  …
10 }

 Листинг 6.  Файл “actions.js” с описанием действий

1.4. Использование сервисов в компонентах

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

Рассмотрим пример страницы «Экипировка пользователя», в которой используется сервис Items. Код страницы приведен в Листинге 7. Я пока намеренно не буду говорить о секциях <template> и <styles>, сосредоточившись на логике. О том, как мы реализовывали отображение, я расскажу во второй части данной статьи.

1 <template lang="pug">
2     ...
3     g-card-list(
4         :items="items"
5         @equip="equip($event)"
6         @un-equip="unEquip($event)"
7         @sell="sell($event)"
8     )
9 ...
10 </template>
11 <script>
12 import { mapState } from 'vuex'
13 import ItemsService from '~/services/items'
14 export default {
15   name: 'InventoryPage',
16   layout: 'default',
17   transition: 'slide-fade',
18   data () {
19     return {
20       currentItem: null,
21       sellItem: null,
22       ...
23     }
24   },
25   computed: {
26     ...mapState({
27       items: state => state.items
28     })
29   },
30   mounted () {
31     ItemsService.getList()
32   },
33   methods: {
34    equip (item) {
35      this.$wd.show()
36      ItemsService.equip(item)
37         .then(() => { this.$wd.hide() })
38         .catch((error) => {
39           this.showMessage('Не удалось надеть предмет.')
40           this.error(error)
41         })
42     },
43     unEquip (item) {
44       this.$wd.show()
45       ItemsService.unEquip(item)
46         .then(() => { this.$wd.hide() })
47         .catch((error) => {
48           this.showMessage('Не удалось снять предмет.')
49           this.error(error)
50         })
51     },
52     sell () {
53       this.$wd.show()
54       ItemsService.sell(item)
55         .then(() => {
56           this.showConfirmDialog = false
57           this.$wd.hide()
58         })
59         .catch((error) => {
60           this.showMessage('Не удалось продать предмет.')
61           this.error(error)
62         })
63    }
64   }}
65 </script>

 Листинг 7.  Файл “actions.js” с описанием действий

Расставим акценты на основных моментах, которые реализованы в данном коде:

  1. В хуке mounted() осуществляется загрузка данных об экипировке героя с помощью функции getList() сервиса ItemsService (строка 31)

  2. После выполнения функции getList() сервис сохраняет данные в Vuex. Эти данные уже типизированы, так как сервис использует model перед сохранением в store

  3. В computed свойстве подключается state, который обеспечивает доступ к items на данной странице с использованием mapState (строка 26)

  4. Отображением содержимого страницы и обработкой событий занимается компонент g-card-list (строки 3-8), которому передается через props массив items. При возникновении различных событий (@equip, @un-equip, @sell) происходит вызов соответствующих методов, которые обеспечивают всю логику работы.

1.5. Обработка событий и локальное кэширование данных

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

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

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

Рисунок 2. Модель работы с данными
Рисунок 2. Модель работы с данными

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

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

  2. Обновление страницы пользователем или переход на новую страницу приложения

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

  4. Событие на стороне сервера, например, уведомление о новых заданиях

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

Кэширование данных было реализовано достаточно просто. Для каждого справочника вычисляется hash, как однонаправленная функция. При внесении изменений на беке, информация пересчитывается hash и записывается в таблицу hashes. Фронтенд-приложение анализирует изменения, и если hash не изменился, то данные справочника загружаются из Localstorage. Если же hash изменился, то они запрашиваются с сервера, заново вычисляется hash и данные записываются  локально. При следующей загрузке процесс повторяется.

Исходный код сервиса, который это реализует, показан в Листинге 8.

1 import BaseService from '~/services/base'
2 import ApiService from '~/services/api'
3 import StorageService from '~/services/storage'
4 import HashModel from '~/models/hash'
5
6 export default class Hashes extends BaseService {
7   static getHashes (params = {}) {
8     return ApiService.getList('/hashes', params)
9      .then(hashesData => {
10         const hashes = []
11        hashesData.forEach(hashData => {
12          hashes.push(new HashModel(hashData))
13        })
14         this.vuex.dispatch('setHashes', hashes)
15      })
16   }
17
18  static getLocalHashByName (name) {
19     const hashes = StorageService.get('hashes')
20     const find = hashes 
21      ? hashes.find(hash => hash.name === name) 
22        : null
23     return find ? find.hash : null
24   }
25
26  static setLocalHashByName (name, hash) {
27     let hashes = []
28     hashes = StorageService.get('hashes') || []
29     const find = hashes 
30      ? hashes.find(hash => hash.name === name) 
31      : null
32     if (find) {
33       find.hash = hash
34     } else {
35       hashes.push({ name, hash })
36     }
37     StorageService.set('hashes',hashes)
38   }
39 }

Листинг 8.  Исходный код сервиса проверки hash

Пример использования данного сервиса при  загрузке справочника Criteria приведен в Листинге 9. Он показывает, как при загрузке данных осуществляется определение неизменности hash и происходит загрузка либо с сервера (строки 17-28), либо из localStorage (строка 5). 

1 export default class Criteria extends BaseService {
2   static getList () {
3     const currentHash = this.vuex.state.hashes
4       .find(_hash => _hash.name === 'skills')
5     const localSkills = StorageService.get('skills')
6     const localHash = 
7       HashesService.getLocalHashByName('skills')
8     if (
9       currentHash && 
10       localHash && 
11       localSkills && 
12       currentHash.hash === localHash
13     ) {
14       return 
15         this.vuex.dispatch('setCriteria', localSkills)
16     } else {
17       return ApiService.getList('skills')
18         .then(criteriaData => {
19           const criteria = []
20           criteriaData.forEach(cd => 
21             criteria.push(new CriterionModel(cd)))
22           this.vuex.dispatch('setCriteria', criteria)
23           if (currentHash && currentHash.hash) {
24             HashesService.setLocalHashByName('skills',
25               currentHash.hash)
26           }
27           StorageService.set('skills', criteria)
28         })
29     }
30   }
31 }

 Листинг 9.  Пример использования сервиса кэширования при загрузке справочника Criteria

После хеширования общее время работы плагина инициализации удалось сократить с 8 секунд до 3 секунд. Результаты приведены в Таблице 1.

 Таблица 1.  Результаты использования плагина кэширования
 Таблица 1.  Результаты использования плагина кэширования

1.6. Итоги реализации по архитектуре проекта

Всего в процессе работы над данным проектом было реализовано 47 моделей, 36 сервисов, 48 страниц, 68 компонентов (о них речь пойдет позже), 2 плагина и store, который содержит данные для всех моделей. Результаты данной реализации показаны на Рисунке 3.

Рисунок 3. Результаты реализации архитектуры приложения
Рисунок 3. Результаты реализации архитектуры приложения

2. Реализация набора визуальных элементов приложения

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

  1. Аватар пользователя

  2. Текущий уровень пользователя

  3. Количество золота

  4. Навигационная панель

  5. Основные навыки персонажа

  6. Таб-панели

  7. Список заданий со скроллом

  8. Прогресс-бар

  9. Предмет

  10. Кнопка

  11. Всплывающая подсказка (ToolTip)

  12. … и так далее по всем экранам, если есть новый элемент, то добавляем его в список

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

Первый проход дал около 60 элементов, однако при дальнейшем анализе удалось сократить их число до 40 за счет вынесения различий в свойства компонентов (props) и наличия динамически задаваемых CSS-классов Vue JS.

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

  • Тип 1: Простые элементы — их внешний вид и функциональность можно обеспечить только за счет изменения CSS свойств стандартного Vuetify компонента  (либо композиции стандартных Vuetify компонентов)

  • Тип 2: Сложные элементы — требуют глубокого переопределения свойств CSS стандартного Vuetify компонента за счет механизма наследования. Могут содержать анимацию

  • Тип 3: Составные элементы — состоят из набора простых и сложных элементов, описанных выше. Как правило, содержат часть логики поведения, заданной с помощью Vue.js + анимацию

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

2.1. Реализация простого компонента на примере элемента пагинации

Компонент пагинации используется для переключения номеров страниц в табличном представлении длинных списков. Его хорошая реализация есть в UI FrameWork Vuetify. Стандартный вид этого компонента показан на Рисунке 4.

Рисунок 4. Внешний вид типового элемента пагинации Vuetify
Рисунок 4. Внешний вид типового элемента пагинации Vuetify

Однако, согласно дизайн-макетам, его внешний вид должен быть таким, как показано на Рисунке 5.

Рисунок 5. Внешний вид элемента пагинации для игрового приложения
Рисунок 5. Внешний вид элемента пагинации для игрового приложения

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

1 <template lang="pug">
2 .pagination.d-flex.flex-row
3   v-pagination.justify-start(
4     v-model="currentPage"
5     :length="length"
6     :total-visible="totalVisible"
7    @input="$emit('current-page-change', currentPage)"
8   )
9 </template>
10 <script>
11 export default {
12   props: {
13     page: {
14       required: true,
15       type: Number
16     },
17     totalVisible: {
18       required: true,
19       type: Number
20     },
21     perPage: {
22       required: true,
23       type: Number
24     },
25     length: {
26       required: true,
27       type: Number
28     }
29   },
30   data () {
31     return {
32       currentPage: null
33     }
34   },
35   watch: {
36     page () {
37       this.currentPage = this.page
38     }
39   },
40   created () {
41     this.currentPage = this.page
42  }
43 }
44 </script>
45 
46 <style lang="stylus" scoped>
47  
48 @import '~assets/css/variables'
49 .v-pagination
50  
51   .v-pagination__navigation, .v-pagination__item
52     background none
53     box-shadow none
54     outline none
55 
56   .v-pagination__item, .v-pagination__more
57     font-family 'TT Norms Medium'
58     font-size 8px
59     color $gray-brown-color
60  
61   .v-pagination__more
62     padding 0 0 12px 0
63     letter-spacing 2px
64  
65   .v-pagination__navigation .v-icon:before
66     color #625E54
67  
68   .v-pagination__navigation, .v-pagination__more
69     margin 0 2px
70  
71   .v-pagination__item
72     width 38px
73     height 28px
74     display flex
75     align-items center
76     justify-content center
77     position relative
78     z-index 10
79     padding 0 10px
80     margin 0 5px
81  
82     &:before
83       content ''
84       width 100%
85       height 100%
86       position absolute
87       left 0
88       transform skew(155deg)
89       z-index -10
90       border 1px solid #625E54
91       transition 0.1s
92 
93      &.v-pagination__item--active, &:hover
94        color #101113
95        font-weight bold
96 
97        &:before
98           background #C57200
99 </style>

Листинг 10.  Компонент g-pagination

Мы получили простой однофайловый Vue-компонент, который содержит три стандартные секции: template, script и style. Для простых элементов первого типа основной интерес представляет секция style, в которой определяются цвета и внешний вид элемента пагинации (строки 71-98). Свойства props (строки 12-29) данного компонента просто дублируют необходимые свойства стандартного компонента v-pagination:

  • page — текущая выбранная страница

  • length — общее количество элементов списка

  • total-visible — количество отображаемых элементов пагинатора

  • per-page — количество элементов, которое показывается на одной странице

Также компонент g-pagination может эмитировать событие current-page-change (строка 6), которое используется для обработки действия по переключению страниц, если, например, используется серверный вариант пагинации.

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

1 g-pagination(
2 @current-page-change="switchPage($event)"
3   :page="currentPage"
4   :length="25":total-visible="4"
5   :per-page="10"
6 )

 Листинг 11. Пример использования компонента g-pagination

 2.2. Пример реализации сложного компонента на примере кнопки

Компонент для отображения кнопки — самый повторяемый элемент интерфейса. Согласно дизайн-макетам, он может быть представлен в нескольких вариантах. Эти варианты показаны на Рисунке 6. 

Рисунок 6. Различные виды кнопок для игрового интерфейса
Рисунок 6. Различные виды кнопок для игрового интерфейса

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

1 <template lang="pug">
2 .button-container
3   v-btn.button(
4     :disabled="disabled"
5     :class="classes"
6     :width="width"
7     ref="button"
8   )
9     slot
10 </template>
11
12 <script>
13 import Vue from 'vue'
14 const VBtn = Vue.options.components["VBtn"]
15 
16 export default {
17   extends: VBtn,
18  props: {
19     accent: {
20       default: false,
21       type: Boolean
22     },
23     orange: {
24       default: false,
25       type: Boolean
26     },
27     long: {
28       default: false,
29       type: Boolean
30     },
31     yellow: {
32       default: false,
33       type: Boolean
34     },
35     gold: {
36       default: false,
37       type: Boolean
38     },
39     width: {
40       default: undefined,
41       type: String
42     }
43   },
44   computed: {
45    classes () {
46       const classes = { long: this.long }
47       if (!this.yellow && !this.orange && 
48         !this.gold && !this.accent && !this.disabled) {
49         classes['g-btn--gray'] = true
50       } else if (this.orange) {
51        classes['g-btn--orange'] = true
52       } else if (this.yellow) {
53        classes['g-btn--yellow'] = true
54       } else if (this.gold) {
55         classes['g-btn--gold'] = true
56       } else if (this.accent) {
57         classes['g-btn--accent'] = true
58       } else if (this.disabled) {
59         classes['g-btn--disabled'] = true
60       }
61       return classes
62     }
63   }
64 }
65 </script>
66
67 <style lang="stylus" scoped>
68 @import '~assets/css/variables'
69
70 .button-container
71   button.button.v-btn:not(.v-btn--flat):not(.v-btn--text)
72   :not(.v-btn--outlined)
73     background-color transparent !important
74     padding 0 24px
75     box-shadow none
76     margin 0 10px
77
78     &.long
79       padding 0 36px
80
81     &:before, &:after
82       content ''
83       position absolute
84       transform skew(150deg)
85       border-radius initial
86       background-color: transparent;
87       opacity 1
88
89     &:before
90       width 100%
91       height 100%
92 
93     &:after
94       width calc(100% - 8px)
95       height calc(100% - 8px)
96       box-shadow none
97 
98     ::v-deep .v-btn__content
99      position relative
100       z-index 10
101 
102     // GRAY
103     &.g-btn--gray
104       ::v-deep .v-btn__content
105         color $gray-color
106         font-size 8px
107         font-weight 800
108 
109       &:before
110         border 2px solid #45433E
111 
112       &:hover
113         ::v-deep .v-btn__content
114           color #E0DACA
115 
116         &:before
117           border 2px solid #A39D8C
118 
119     // ACCENT
120     …
121     // GOLD
122     …
123     // ORANGE
124     …
125     // YELLOW
126     …
127     // DiSABLED
128     …
129   </style>

 Листинг 12. Реализация компонента g-btn

Обратите внимание на строки 14, 15 и 17. Здесь осуществляется импортирование стандартных свойств компонента v-btn. Computed свойство classes (строки 44-60) осуществляет применение CSS класса в зависимости от свойств компонента. Сами CSS свойства для компонента определены в секции style. Здесь приведен пример определения стиля для активной серой кнопки GRAY (строки 102 -117) с помощью свойства “gray”. Остальные свойства для ACCENT, GOLD, ORANGE, YELLOW и DISABLED определяются аналогичным способом. В Листинге 13 приведены примеры использования компонента g-btn на странице приложения.

1 ...
2 g-btn(
3   gold
4 @click.native="$emit('close')"
5 ) Закрыть
6 g-btn(
7   gray
8 @click.native="$emit('cancel')"
9 ) Отмена
10 ...

Листинг 13. Пример использования  компонента g-btn

2.3. Реализация составного компонента на примере g-card-list

Следующим этапом работы стал этап формирования списка композиционных компонентов. Это компоненты, которые состоят из комбинации простых, сложных типов элементов, а также, возможно, других стандартных компонентов Vuetify. Рассмотрим такой элемент на примере реализации компонента g-card-list-item (Рисунок 7).

Рисунок 7. Визуальное представление компонента g-card-list-item («Карточка продукта»)
Рисунок 7. Визуальное представление компонента g-card-list-item («Карточка продукта»)

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

  • Изображения предмета — это рамка + картинка самого предмета

  • Стоимости предмета — стоимость предмета + символ монетки

  • Названия предмета — «Редкий»

  • Уровня предмета — это ромбики в нижней части, число и цвет которых зависят от уровня и типа компонента

Исходный код этого компонента приведен в Листинге 14. Он также оформлен в виде готового компонента Vue для удобства его использования.

1 <template lang="pug">
2 .d-flex
3   .card-list
4     .item__title {{ item.title }}
5     g-item(
6       :item="item"
7       :active="active"
8       :progress="false"
9     ).item__wrapper
10     g-resource(
11       :value="item.price"
12       icon="gold"
13       color="gold"
14       small
15       reverse
16     )
17     g-level(
18       :level="item.level"
19     )
20 </template>
21 
22 <script>
23 import BaseService from '~/services/base'
24 const LIST_TYPE_GAME = BaseService.LIST_TYPE_GAME
25 
26 export default {
27   props: {
28     item: {
29       required: true,
30       type: Object
31     },
32     active: {
33       default: false,
34       type: Boolean
35     },
36     progress: {
37       default: false,
38       type: Boolean
39     },
40     type: {
41       default: LIST_TYPE_GAME,
42       type: String
43     }
44   },
45   data () {
46     return {
47       LIST_TYPE_GAME
48     }
49   }
50 }
51 </script>
52 
53 <style lang="stylus" scoped>
54 @import '~assets/css/variables'
55 
56 .card-list
57   width 120px
58   height 80px
59   position relative
60   background url('~assets/svg/rb.svg')
61 
62   .item__wrapper
63     width 40px
64     height 40px
65 
66   .item__title
67     font-family 'TT Norms Medium'
68     text-transform uppercase
69     font-size 8px
70     text-align center
71     color $gold-1-color
72 </style>

Листинг 14. Реализация компонента g-card-list-item

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

  • g-item — реализует центральную часть данной визуализации

  • g-resource — содержит описание и стоимость компонента

  • g-level — выводит информацию об уровне компонента

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

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

2.4. Реализация анимации

Все анимации в приложении построены на трех стандартных принципах:

  • Свойствах CSS transitions и animation

  • Использовании компонента-обертки transition VueJS

  • Механизме keyframes

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

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

Пример использования компонента обертки  transition VueJS также можно увидеть в Листинге 7, строка 17. Типовые варианты использования такой анимации могут обеспечивать красивые эффекты при переключении страниц или изменении отдельных компонентов. Информацию по их использованию можно также получить из документации.

Рассмотрим пример реализации анимации с использованием механизма keyframes.  Данный вид анимации был применен для экрана реализации поединка между двумя соперниками.  Исходный код компонента, реализующего этот способ, приведен в Листинге 15. 

1 <template lang="pug">
2   div.avatar
3     img.avatar-img(
4       src="~assets/images/avatar.png"
5       :class="{ left, right }"
6     )
7     .damage(
8       v-if="damage"
9       :class="{ left, right }"
10     )
11       .text {{ damage.text }}
12       .value {{ damage.value | add-sign }}
13 
14     .recovery(
15       v-if="recovery"
16       :class="{ left, right }"
17     )
18       .text {{ recovery.text }}
19       .value {{ recovery.value | add-sign }}
20 
21 </template>
22 
23 <script>
24 export default {
25   props: {
26     avatar: {
27      type: String,
28       default: () => 'male'
29     },
30    side: {
31       type: String,
32       default: () => 'left'
33     },
34     red: {
35       type: Boolean,
36       defalult: () => false
37     },
38     damage: {
39       type: Object,
40       default: () => (null)
41     },
42     recovery: {
43       type: Object,
44       default: () => (null)
45     }
46   },
47   computed: {
48     left () {
49       return this.side === 'left'
50     },
51     right () {
52      return this.side === 'right'
53    }
54 
55   }
56 }
57 </script>
58 <style lang="stylus" scoped>
59 @import '~assets/css/variables'
60
61 .avatar
62   width 450px
63   height 550px
64   padding 92px 0
65 
66   &-img
67     height 428px
68 
69     &.animated
70       animation hit 0.3s linear
71 
72     &.left
73       float right
74 
75     &.right
76       float left
77
78   .damage
79     position absolute
80     margin-top 150px
81     width 450px
82 
83     &.left
84       text-align left
85 
86    &.right
87      text-align right
88
89    .text
90       font-size 16px
91       color $red-color
92 
93     .value
94       font-size 52px
95       line-height 56px
96       letter-spacing -0.1px
97       color $red-color
98 
99   .recovery
100     position absolute
101     margin-top 150px
102     width 450px
103
104     &.left
105       text-align left
106
107     &.right
108       text-align right
109 
110   .text
111       font-size 16px
112       color #72875C
113
114     .value
115       font-size 52px
116       line-height 56px
117       letter-spacing -0.1px
118       color #72875C
119
120 @keyframes hit {
121   0% { transform: scale(1) }
122   50% { transform: scale(0.95) }
123   100% { transform: scale(1) }
124 }
124 </style>

 Листинг 15. Реализация анимации с помощью keyframes

Приведенный выше код показывает пример использования keyframes с именем hit, который применяется в качестве анимации. Определение самой анимации можно увидеть в строках 120-124, а применение данной анимации в строке 70.

Итоги

Разработка данного приложения длилась четыре месяца. В команду входили project-менеджер, аналитик, дизайнер, архитектор, два backend-разработчика, два frontend-разработчика и QA-специалист. 

Что было сделано с технической точки зрения:

  1. Реализовали игровое приложение с помощью Nuxt, которое содержит около 40 типовых компонентов и около 20 анимаций. Работа по классификации и систематизации графических элементов позволила повысить эффективность работы за счет композиции и повторного использования кода.

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

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

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

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

Спасибо за внимание!

Авторские материалы для разработчиков и архитекторов мы также публикуем в наших соцсетях – ВКонтакте и Telegram.

Теги:
Хабы:
Всего голосов 5: ↑4 и ↓1+4
Комментарии11

Публикации

Информация

Сайт
www.simbirsoft.com
Дата регистрации
Дата основания
Численность
1 001–5 000 человек
Местоположение
Россия