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

Vue.js

Перевод Vue.js для начинающих, урок 5 обработка событий

21.07.2020 16:21:44 | Автор: admin
Сегодня, в пятом уроке курса по Vue.js для начинающих, речь пойдёт о том, как обрабатывать события.



Vue.js для начинающих, урок 1: экземпляр Vue
Vue.js для начинающих, урок 2: привязка атрибутов
Vue.js для начинающих, урок 3: условный рендеринг
Vue.js для начинающих, урок 4: рендеринг списков

Цель урока


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

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

Начальный вариант кода


В файле проекта index.html будет присутствовать следующий код:

<div id="app"><div class="product"><div class="product-image"><img :src="image" /></div><div class="product-info"><h1>{{ product }}</h1><p v-if="inStock">In stock</p><p v-else>Out of Stock</p><ul><li v-for="detail in details">{{ detail }}</li></ul><div v-for="variant in variants" :key="variant.variantId"><p>{{ variant.variantColor }}</p></div></div></div></div>

Вот содержимое main.js:

var app = new Vue({el: '#app',data: {product: "Socks",image: "./assets/vmSocks-green.jpg",inStock: true,details: ['80% cotton', '20% polyester', 'Gender-neutral'],variants: [{variantId: 2234,variantColor: "green"},{variantId: 2235,variantColor: "blue"}]}})

Задача


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

Решение


Для начала добавим, в main.js, в объект data, новое свойство, которое будет символизировать количество товара в корзине:

cart: 0

Теперь, в index.html, добавим элемент <div>, описывающий корзину. В этом элементе будет использован тег <p>, с помощью которого на страницу будет выводиться число, хранящееся в свойстве cart:

<div class="cart"><p>Cart({{ cart }})</p></div>

Ещё мы создадим в коде index.html кнопку, которая позволяет добавлять товар в корзину:

<button v-on:click="cart += 1">Add to cart</button>

Здесь обратите внимание на то, что для инкрементирования значения, хранящегося в cart, мы используем директиву v-on.


Страница с корзиной и с кнопкой для добавления товара в корзину

Если теперь нажать на кнопку количество товара в корзине увеличится на 1.

Как всё это работает?

Давайте разберёмся в представленной здесь конструкции. Использование директивы v-on сообщает Vue о том, что мы хотим прослушивать события, происходящие с кнопкой. Потом идёт двоеточие, после которого указывается то, какое конкретно событие нас интересует. В данном случае это событие click. В кавычках записано выражение, которое добавляет 1 к значению, хранящемуся в cart. Это происходит при каждом щелчке по кнопке.

Это простой, но не вполне реалистичный пример. Вместо того, чтобы указывать в кавычках выражение cart += 1, давайте сделаем так, чтобы щелчок по кнопке вызывал бы метод, который будет увеличивать значение, хранящееся в cart. Вот как это выглядит:

<button v-on:click="addToCart">Add to cart</button>

Как видите, здесь addToCart это имя метода, который будет вызван при возникновении события click. Но сам метод мы пока не объявили, поэтому давайте сделаем это прямо сейчас, оснастив им наш экземпляр Vue.

Тут используется механизм, очень похожий на тот, который мы уже применяем для хранения данных. А именно, речь идёт о том, что у объекта с опциями, используемого при создании экземпляра Vue, может быть необязательное свойство, носящее имя methods, в котором содержится объект с методами. В нашем случае это будет всего один метод addToCart:

methods: {addToCart() {this.cart += 1}}

Теперь, когда мы щёлкаем по кнопке, вызывается метод addToCart, который и увеличивает значение cart, выводящееся в теге <p>.

Продолжим разбор того, что здесь происходит.

Кнопка прослушивает события click благодаря директиве v-on, которая вызывает метод addToCart. Этот метод находится в свойстве methods экземпляра Vue. В теле функции содержится инструкция, добавляющая 1 к значению this.cart. Так как this хранит ссылку на то место, где хранятся данные экземпляра Vue, в котором мы находимся, функция добавляет 1 к значению cart. А this.cart это то же самое, что и свойство cart, объявленное в свойстве data объекта с опциями.

Если бы мы просто написали бы в теле функции что-то вроде cart += 1, то мы столкнулись бы с сообщением об ошибке cart is not defined. Именно поэтому мы используем конструкцию this.cart и обращаемся к cart из экземпляра Vue, используя this.

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

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

Для начала давайте расширим объекты массива variants из объекта data, добавив туда свойство variantImage, хранящее путь к изображению нужного варианта товара. Приведём соответствующий раздел файла main.js к такому виду:

variants: [{variantId: 2234,variantColor: "green",variantImage: "./assets/vmSocks-green.jpg"},{variantId: 2235,variantColor: "blue",variantImage: "./assets/vmSocks-blue.jpg"}],

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

Задача


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

Решение


Тут нам снова пригодится директива v-on. Но в этот раз мы воспользуемся сокращённым вариантом её записи, который выглядит как @. А прослушивать будем событие mouseover.

Вот соответствующий код в index.html:

<div v-for="variant in variants" :key="variant.variantId"><p @mouseover="updateProduct(variant.variantImage)">{{ variant.variantColor }}</p></div>

Обратите внимание на то, что мы передаём методу updateProduct, в виде аргумента, variant.variantImage.

Создадим этот метод в main.js:

updateProduct(variantImage) {this.image = variantImage}

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

Но тут мы обновляем значение, хранящееся в image. А именно, в image записывается то, что хранится в variantImage того варианта товара, на который наведён указатель мыши. Соответствующее значение передаётся функции updateProduct из самого обработчика события, находящегося в index.html:

<p @mouseover="updateProduct(variant.variantImage)">

Другими словами, теперь метод updateProduct готов к вызову с параметром variantImage.

Когда вызывается этот метод, variant.variantImage передаётся ему в виде variantImage и используется для обновления значения, хранящегося в this.image. Мы, по аналогии с ранее рассмотренной конструкцией this.cart, можем сказать, что this.image это то же самое, что image. В результате значение, хранящееся в image, теперь динамически обновляется в соответствии с данными варианта товара, на который наведён указатель мыши.

Синтаксис ES6


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

updateProduct(variantImage) {this.image = variantImage}

Это сокращённый вариант описания методов, который появился в ES6. Более старый вариант записи подобных конструкций выглядит так:

updateProduct: function(variantImage) {this.image = variantImage}

Практикум


Создайте кнопку и соответствующий метод, которые позволят уменьшать значение, хранящееся в cart.

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

Вот решение задачи.

Итоги


Подведём итоги сегодняшнего занятия:

  • Для организации реакции элемента на события используется директива v-on.
  • Сокращённый вариант директивы v-on выглядит как @.
  • При использовании v-on можно указать тип прослушиваемого события:

    • click
    • mouseover
    • любое событие DOM
  • Директива v-on может вызывать методы.
  • Метод, вызываемый с помощью v-on, может принимать аргументы.
  • Ключевое слово this содержит ссылку на то место, где хранятся данные текущего экземпляра Vue. Его использование позволяет работать с данными экземпляра, а так же с методами, объявленными в экземпляре.

Получилось ли у вас домашнее задание к этому уроку?

Vue.js для начинающих, урок 1: экземпляр Vue
Vue.js для начинающих, урок 2: привязка атрибутов
Vue.js для начинающих, урок 3: условный рендеринг
Vue.js для начинающих, урок 4: рендеринг списков

Подробнее..

Перевод Vue.js для начинающих, урок 6 привязка классов и стилей

24.07.2020 16:16:55 | Автор: admin
Сегодня, в шестом уроке курса по Vue, мы поговорим о том, как динамически стилизовать HTML-элементы, привязывая данные к их атрибутам style и привязывая к элементам классы.



Vue.js для начинающих, урок 1: экземпляр Vue
Vue.js для начинающих, урок 2: привязка атрибутов
Vue.js для начинающих, урок 3: условный рендеринг
Vue.js для начинающих, урок 4: рендеринг списков
Vue.js для начинающих, урок 5: обработка событий

Цель урока


Первой целью данного урока будет использование цвета, соответствующего вариантам товаров, для настройки свойства background-color элементов <div>, выводящих сведения об этих вариантах. Так как вариантам товара соответствуют цвета green и blue, нам нужно, чтобы один элемент <div> имел бы зелёный фоновый цвет, а второй синий.

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

Начальный вариант кода


Вот как выглядит сейчас код, находящийся в index.html:

<div id="app"><div class="product"><div class="product-image"><img :src="image" /></div><div class="product-info"><h1>{{ product }}</h1><p v-if="inStock">In stock</p><p v-else>Out of Stock</p><ul><li v-for="detail in details">{{ detail }}</li></ul><div v-for="variant in variants" :key="variant.variantId"><p @mouseover="updateProduct(variant.variantImage)">{{ variant.variantColor }}</p></div><button v-on:click="addToCart">Add to cart</button><div class="cart"><p>Cart({{ cart }})</p></div></div></div></div>

Вот что сейчас находится в main.js:

var app = new Vue({el: '#app',data: {product: "Socks",image: "./assets/vmSocks-green.jpg",inStock: true,details: ['80% cotton', '20% polyester', 'Gender-neutral'],variants: [{variantId: 2234,variantColor: "green",variantImage: "./assets/vmSocks-green.jpg"},{variantId: 2235,variantColor: "blue",variantImage: "./assets/vmSocks-blue.jpg"}],cart: 0},methods: {addToCart() {this.cart += 1},updateProduct(variantImage) {this.image = variantImage}}})

Задача


В предыдущем уроке мы создали обработчик событий, который меняет изображение товара, основываясь на том, на какой элемент <p> был наведён указатель мыши. Вместо того чтобы выводить название цвета в элементе <p>, мы хотели бы использовать этот цвет для настройки свойства background-color соответствующего элемента <div>. При таком подходе, вместо того, чтобы наводить мышь на тексты, мы сможем наводить её на цветные квадраты, что приведёт к выводу на странице изображения товара, цвет которого соответствует цвету, показанному в квадрате.

Решение


Для начала давайте добавим к элементу <div> класс color-box, который задаёт его ширину, высоту и внешний верхний отступ. Так как мы, даже сделав это, продолжаем выводить в элементах <div> слова green и blue, мы можем взять названия цветов, хранящихся в объектах, описывающих варианты товара, и использовать эти названия при привязке стиля к атрибуту style. Вот как это выглядит:

<divclass="color-box"v-for="variant in variants":key="variant.variantId":style="{ backgroundColor:variant.variantColor }"><p @mouseover="updateProduct(variant.variantImage)">{{ variant.variantColor }}</p></div>

Обратите внимание на вторую и пятую строки этого кода. Здесь мы добавляем к элементу <div> класс color-box и привязываем к нему встроенный стиль. Встроенный стиль здесь используется для динамической настройки свойства background-color элементов <div>. Цвет для фона элементов берётся из variant.variantColor.


Стилизованные элементы <div> и выводимые на них надписи

Теперь, когда элемент <div> стилизован с использованием variantColor, нам больше не нужно выводить в нём название цвета. Поэтому мы можем избавиться от тега <p> и переместить конструкцию @mouseover=updateProduct(variant.variantImage) в сам элемент <div>.

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

<divclass="color-box"v-for="variant in variants":key="variant.variantId":style="{ backgroundColor:variant.variantColor }"@mouseover="updateProduct(variant.variantImage)"></div>


Стилизованные элементы <div> без текста

Теперь при наведении мыши на синий квадрат на странице выводится изображение синих носков. А при наведении мыши на зелёный квадрат изображение зелёных носков. Красота!

Разобравшись с привязкой стилей, поговорим о привязке классов.

Задача


Сейчас в наших данных есть следующее:

inStock: true,

Когда свойство inStock принимает значение false, нам нужно запретить посетителям сайта щёлкать по кнопке Add to Cart, так как на складе нет товара, а значит, его нельзя добавить в корзину. К нашей удаче, существует специальный HTML-атрибут, носящий имя disabled, с помощью которого можно отключить кнопку.

Если вспомнить материал второго урока, то окажется, что мы можем воспользоваться техникой привязки атрибутов для добавления к элементу атрибута disabled тогда, когда inStock равняется false, или, скорее, в случае, когда это значение не является истинным (!inStock). Перепишем код кнопки:

<buttonv-on:click="addToCart":disabled="!inStock">Add to cart</button>

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


Отключённая кнопка выглядит так же, как обычная, но щёлкать по ней бессмысленно

Решение


Тут мы поступим, действуя по той же схеме, по которой действовали, привязывая inStock к атрибуту disabled. А именно, будем привязывать класс disabledButton к нашей кнопке в случаях, когда inStock хранит false. При таком подходе, если по кнопке будет бессмысленно щёлкать, то и выглядеть она будет соответственно.

<buttonv-on:click="addToCart":disabled="!inStock":class="{ disabledButton: !inStock }">Add to cart</button>


Отключённая кнопка выглядит так, как нужно

Как видите, теперь кнопка становится серой в том случае, если inStock равняется false.

Давайте разберёмся в том, что здесь происходит.

Взгляните на эту строчку:

:class="{ disabledButton: !inStock }"

Здесь мы используем сокращённый вариант записи директивы v-bind (:) для организации привязки данных к атрибуту class кнопки. В фигурных скобках мы определяем присутствие класса disabledButton на основании истинности свойства inStock.

Другими словами, когда товара на складе нет (!inStock), к кнопке добавляется класс disabledButton. Так как этот класс задаёт серый фоновый цвет кнопки, кнопка становится серой.

Замечательно! Только что мы скомбинировали наши новые знания, касающиеся привязки классов, со знаниями о привязке атрибутов, и смогли отключить кнопку и делать её серой в том случае, если inStock равняется false.

Дополнительные сведения


К элементу можно привязывать объект классов или массив классов:

<div :class="classObject"></div><div :class="[activeClass, errorClass]"></div>

Практикум


Когда в inStock записано значение false, нужно привязать к тегу <p>, выводящему текст Out of Stock, класс, который добавляет к элементу стиль text-decoration: line-through, перечёркивая текст.

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

Вот решение задачи.

Итоги


Вот самое важное из того, что мы сегодня изучили:

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


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

Vue.js для начинающих, урок 1: экземпляр Vue
Vue.js для начинающих, урок 2: привязка атрибутов
Vue.js для начинающих, урок 3: условный рендеринг
Vue.js для начинающих, урок 4: рендеринг списков
Vue.js для начинающих, урок 5: обработка событий

Подробнее..

Перевод Vue.js для начинающих, урок 7 вычисляемые свойства

28.07.2020 18:07:12 | Автор: admin
Сегодня, в седьмом уроке курса по Vue, мы поговорим о вычисляемых свойствах. Эти свойства экземпляра Vue не хранят значения, а вычисляют их.



Vue.js для начинающих, урок 1: экземпляр Vue
Vue.js для начинающих, урок 2: привязка атрибутов
Vue.js для начинающих, урок 3: условный рендеринг
Vue.js для начинающих, урок 4: рендеринг списков
Vue.js для начинающих, урок 5: обработка событий
Vue.js для начинающих, урок 6: привязка классов и стилей

Цель урока


Нашей основной целью является вывод данных, описываемых свойствами объекта с данными brand и product, в виде единой строки.

Начальный вариант кода


Вот код, находящийся в index.html, в теге <body>, с которого мы начнём работу:

<div id="app"><div class="product"><div class="product-image"><img :src="image" /></div><div class="product-info"><h1>{{ product }}</h1><p v-if="inStock">In stock</p><p v-else :class="{ outOfStock: !inStock }">Out of Stock</p><ul><li v-for="detail in details">{{ detail }}</li></ul><divclass="color-box"v-for="variant in variants":key="variant.variantId":style="{ backgroundColor:variant.variantColor }"@mouseover="updateProduct(variant.variantImage)"></div><buttonv-on:click="addToCart":disabled="!inStock":class="{ disabledButton: !inStock }">Add to cart</button><div class="cart"><p>Cart({{ cart }})</p></div></div></div></div>

Вот код main.js:

var app = new Vue({el: '#app',data: {product: 'Socks',brand: 'Vue Mastery',image: './assets/vmSocks-green.jpg',inStock: true,details: ['80% cotton', '20% polyester', 'Gender-neutral'],variants: [{variantId: 2234,variantColor: 'green',variantImage: './assets/vmSocks-green.jpg'},{variantId: 2235,variantColor: 'blue',variantImage: './assets/vmSocks-blue.jpg'}],cart: 0},methods: {addToCart() {this.cart += 1},updateProduct(variantImage) {this.image = variantImage}}})

Обратите внимание на то, что в объект с данными добавлено новое свойство с именем brand.

Задача


Нам надо, чтобы то, что хранится в brand и в product, было бы скомбинировано в одну строку. Другими словами, нам нужно вывести в теге <h1> текст Vue Mastery Socks, а не просто Socks. Для решения этой задачи нужно задаться вопросом о том, как можно конкатенировать два строковых значения, хранящихся в экземпляре Vue.

Решение задачи


Мы для решения этой задачи воспользуемся вычисляемыми свойствами. Так как эти свойства не хранят значения, а вычисляют их, давайте добавим в объект с опциями, используемый при создании экземпляра Vue, свойство computed и создадим вычисляемое свойство с именем title:

computed: {title() {return this.brand + ' ' + this.product;}}

Полагаем, тут всё устроено очень просто и понятно. Когда вызывается метод title(), он выполняет конкатенацию строк brand и product, после чего возвращает полученную в результате новую строку.

Теперь нам осталось лишь вывести title в теге <h1> нашей страницы.

Сейчас этот тег выглядит так:

<h1>{{ product }}</h1>

А теперь мы сделаем его таким:

<h1>{{ title }}</h1>

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


Заголовок страницы изменился

Как видно, в заголовке выводится Vue Mastery Socks, а это значит, что мы всё сделали правильно.

Мы взяли два значения из данных экземпляра Vue и создали на их основе новое значение. Если значение brand когда-нибудь будет обновлено, например в это свойство окажется записанной строка Vue Craftery, то вносить какие-то изменения в код вычисляемого свойства не потребуется. Это свойство будет продолжать возвращать корректную строку, которая теперь будет выглядеть как Vue Craftery Socks. В вычисляемом свойстве title всё ещё будет использоваться свойство brand, так же, как и раньше, но теперь в brand будет записано новое значение.

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

Более сложный пример


Сейчас мы обновляем изображение, выводимое на странице, используя метод updateProduct. Мы передаём ему variantImage, а затем записываем в свойство image то, что попало в метод после наведения мыши на соответствующий цветной квадрат. Соответствующий код выглядит так:

updateProduct(variantImage) {this.image = variantImage;}

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

А именно, вместо того, чтобы хранить в данных свойство image, заменим его на свойство selectedVariant. Инициализируем его значением 0.

selectedVariant: 0,

Почему 0? Дело в том, что мы планируем устанавливать это свойство на основе индекса (index) элемента, над которым находится указатель мыши. Мы можем добавить индекс в конструкцию v-for:

<divclass="color-box"v-for="(variant, index) in variants":key="variant.variantId":style="{ backgroundColor:variant.variantColor }"@mouseover="updateProduct(variant.variantImage)"></div>

Обратите внимание на то, что там, где раньше была конструкция v-for=variant in variants, теперь находится код v-for=(variant, index) in variants.

Теперь, вместо того, чтобы передавать variant.variantImage в updateProduct, передадим в этот метод index:

@mouseover="updateProduct(index)"

Теперь займёмся кодом метода updateProduct. Здесь мы получаем индекс. И, вместо записи нового значения в this.image, запишем index в this.selectedVariant. То есть, в selectedVariant попадёт значение index, соответствующее тому квадрату, на который был наведён указатель мыши. Ещё мы, в целях отладки, поместим в этот метод команду для логирования значения index.

updateProduct(index) {this.selectedVariant = index;console.log(index);}

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


Проверка работоспособности созданного нами механизма

Однако изображение теперь на странице не выводится. В консоли появляется предупреждение.


Предупреждение, выводимое в консоль

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

image() {return this.variants[this.selectedVariant].variantImage;}

Здесь мы возвращаем свойство variantImage элемента массива this.variants[this.selectedVariant]. В качестве индекса, по которому осуществляется доступ к элементу массива, используется свойство this.selectedVariant, которое равняется 0 или 1. Это, соответственно, даёт нам доступ к первому или ко второму элементу массива.

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

Сейчас, когда мы подвергли рефакторингу код метода updateProduct, который теперь обновляет состояние свойства selectedVariant, мы можем поработать и с другими данными, хранящимися в объектах из массива variants, с такими, как поле variantQuantity, которое мы сейчас добавим в объекты:

variants: [{variantId: 2234,variantColor: 'green',variantImage: './assets/vmSocks-green.jpg',variantQuantity: 10},{variantId: 2235,variantColor: 'blue',variantImage: './assets/vmSocks-blue.jpg',variantQuantity: 0}],

Давайте избавимся от обычного свойства inStock и, как и при работе со свойством image, создадим новое вычисляемое свойство с тем же именем, значение, возвращаемое которым, будет основываться на selectedVariant и variantQuantity:

inStock(){return this.variants[this.selectedVariant].variantQuantity}

Это свойство очень похоже на вычисляемое свойство image. Но теперь мы берём из соответствующего объекта не свойство variantImage, а свойство variantQuantity.

Если теперь навести указатель мыши на квадрат, количество товара, соответствующее которому, равняется нулю, в inStock попадёт 0, а 0 является в JavaScript значением, приводимым к логическому значению false. Из-за этого на странице будет выведено сообщение Out of Stock.

Обратите внимание на то, что кнопка тоже, как и ранее, правильно реагирует на установку inStock в 0.


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

Почему всё продолжает правильно работать? Дело в том, что inStock всё ещё используется для привязки класса disableButton к нашей кнопке. Единственное различие нового варианта приложения и его предыдущего варианта заключается в том, что теперь inStock это вычисляемое, а не обычное свойство.

Дополнительные сведения о вычисляемых свойствах


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

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

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

Практикум


Добавьте в объект с данными, используемый при создании экземпляра Vue, новое логическое свойства onSale. Оно будет указывать на то, проводится ли распродажа. Создайте вычисляемое свойство sale, которое, на основе brand, product и onSale формирует строку, сообщающую о том, проводится ли сейчас распродажа или нет. Выведите эту строку в карточке товара.

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

Вот решение задачи

Итоги


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

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


Если вы выполняете домашние задания к этому курсу, расскажите, делаете ли вы исключительно то, что вам предлагается, или идёте дальше?

Vue.js для начинающих, урок 1: экземпляр Vue
Vue.js для начинающих, урок 2: привязка атрибутов
Vue.js для начинающих, урок 3: условный рендеринг
Vue.js для начинающих, урок 4: рендеринг списков
Vue.js для начинающих, урок 5: обработка событий
Vue.js для начинающих, урок 6: привязка классов и стилей

Подробнее..

Перевод Vue.js для начинающих, урок 8 компоненты

30.07.2020 16:13:15 | Автор: admin
Сегодня, в восьмом уроке курса по Vue, состоится ваше первое знакомство с компонентами. Компоненты это блоки кода, подходящие для многократного использования, которые могут включать в себя и описание внешнего вида частей приложения, и реализацию возможностей проекта. Они помогают программистам в создании модульной кодовой базы, которую удобно поддерживать.



Vue.js для начинающих, урок 1: экземпляр Vue
Vue.js для начинающих, урок 2: привязка атрибутов
Vue.js для начинающих, урок 3: условный рендеринг
Vue.js для начинающих, урок 4: рендеринг списков
Vue.js для начинающих, урок 5: обработка событий
Vue.js для начинающих, урок 6: привязка классов и стилей
Vue.js для начинающих, урок 7: вычисляемые свойства

Цель урока


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

Начальный вариант кода


Вот код файла index.html, находящийся в теге <body>, с которого мы начнём работу:

<div id="app"><div class="product"><div class="product-image"><img :src="image" /></div><div class="product-info"><h1>{{ title }}</h1><p v-if="inStock">In stock</p><p v-else>Out of Stock</p><p>Shipping: {{ shipping }}</p><ul><li v-for="detail in details">{{ detail }}</li></ul><divclass="color-box"v-for="(variant, index) in variants":key="variant.variantId":style="{ backgroundColor: variant.variantColor }"@mouseover="updateProduct(index)"></div><buttonv-on:click="addToCart":disabled="!inStock":class="{ disabledButton: !inStock }">Add to cart</button><div class="cart"><p>Cart({{ cart }})</p></div></div></div></div>

Вот код main.js:

var app = new Vue({el: '#app',data: {product: 'Socks',brand: 'Vue Mastery',selectedVariant: 0,details: ['80% cotton', '20% polyester', 'Gender-neutral'],variants: [{variantId: 2234,variantColor: 'green',variantImage: './assets/vmSocks-green.jpg',variantQuantity: 10},{variantId: 2235,variantColor: 'blue',variantImage: './assets/vmSocks-blue.jpg',variantQuantity: 0}],cart: 0,},methods: {addToCart() {this.cart += 1;},updateProduct(index) {this.selectedVariant = index;console.log(index);}},computed: {title() {return this.brand + ' ' + this.product;},image() {return this.variants[this.selectedVariant].variantImage;},inStock(){return this.variants[this.selectedVariant].variantQuantity;}}})

Задача


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

Решение задачи


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

Вот как в файле main.js регистрируется компонент:

Vue.component('product', {})

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

В экземпляре Vue мы использовали свойство el для организации его привязки к элементу DOM. В случае с компонентом используется свойство template, которое определяет HTML-код компонента.

Опишем шаблон компонента в объекте с опциями:

Vue.component('product', {template: `<div class="product"> // Здесь будет весь HTML-код, который раньше был в элементе с классом product</div>`})

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

Если окажется так, что код шаблона не будет размещаться в единственном корневом элементе, в таком, как элемент <div> с классом product, это приведёт к выводу такого сообщения об ошибке:

Component template should contain exactly one root element

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

Например, следующий шаблон построен правильно, так как он представлен лишь одним элементом:

Vue.component('product', {template: `<h1>I'm a single element!</h1>`})

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

Vue.component('product', {template: `<h1>I'm a single element!</h1><h2>Not anymore</h2>`})

В результате оказывается, что если шаблон должен включать в себя множество элементов, например набор элементов, заключённых в наш <div> с классом product, эти элементы должны быть помещены во внешний элемент-контейнер. В результате в шаблоне будет лишь один корневой элемент.

Теперь, когда в шаблоне находится HTML-код, который раньше был в файле index.html, мы можем добавить в компонент данные, методы, вычисляемые свойства, которые раньше были в корневом экземпляре Vue:

Vue.component('product', {template: `<div class="product"></div>`,data() {return {// тут будут данные}},methods: {// тут будут методы},computed: {// тут будут вычисляемые свойства}})

Как видите, структура этого компонента практически полностью совпадает со структурой экземпляра Vue, с которым мы работали раньше. А вы обратили внимание на то, что data это теперь не свойство, а метод объекта с опциями? Почему это так?

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

Теперь, когда мы переместили код, связанный с товаром, в собственный компонент product, код описания корневого экземпляра Vue будет выглядеть так:

var app = new Vue({el: '#app'})

Сейчас нам осталось лишь разместить компонент product в коде файла index.html. Это будет выглядеть так:

<div id="app"><product></product></div>

Если теперь перезагрузить страницу приложения она примет прежний вид.


Страница приложения

Если теперь заглянуть в инструменты разработчика Vue, там можно заметить наличие сущности Root и компонента Product.


Анализ приложения с помощью инструментов разработчика Vue

А теперь, просто чтобы продемонстрировать возможности многократного использования компонентов, давайте добавим в код index.html ещё пару компонентов product. Собственно говоря, именно так организовано многократное использование компонентов. Код index.html будет выглядеть так:

<div id="app"><product></product><product></product><product></product></div>

А на странице будет выведено три копии карточки товара.


Несколько карточек товара, выведенные на одной странице

Обратите внимание на то, что в дальнейшем мы будем работать с одним компонентом product, поэтому код index.html будет выглядеть так:

<div id="app"><product></product></div>

Задача


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

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

var app = new Vue({el: '#app',data: {premium: true}})

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

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

Как отправить данные, хранящиеся в свойстве premium корневого экземпляра Vue, дочернему элементу, которым является компонент product?

Решение задачи


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

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

Vue.component('product', {props: {premium: {type: Boolean,required: true}},// Тут будут описания данных, методов, вычисляемых свойств})

Обратите внимание на то, что тут используются встроенные возможности Vue по проверке параметров, передаваемых компоненту. А именно, мы указываем то, что типом входного параметра premium является Boolean, и то, что этот параметр является обязательным, устанавливая required в true.

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

<p>User is premium: {{ premium }}</p>

Пока всё идёт нормально. Компонент product знает о том, что он будет получать необходимый для его работы параметр типа Boolean. Мы подготовили место для вывода соответствующих данных.

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

Доработаем код в index.html:

<div id="app"><product :premium="premium"></product></div>

Обновим страницу.


Вывод данных, переданных компоненту

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

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

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

Вышеприведённый рисунок, а именно, надпись User is premium: true, доказывает то, что всё сделано правильно.

Теперь мы убедились в том, что изучаемый нами механизм передачи данных работает так, как ожидается. Если заглянуть в инструменты разработчика Vue, то окажется, что у компонента Product теперь есть входной параметр premium, хранящий значение true.


Входной параметр компонента

Сейчас, когда данные о том, обладает ли пользователь премиум-аккаунтом, попадают в компонент, давайте используем эти данные для того чтобы вывести на странице сведения о стоимости доставки. Не будем забывать о том, что если параметр premium установлен в значение true, то пользователю полагается бесплатная доставка. Создадим новое вычисляемое свойство shipping и воспользуемся в нём параметром premium:

shipping() {if (this.premium) {return "Free";} else {return 2.99}}

Если в параметре this.premium хранится true вычисляемое свойство shipping вернёт Free. В противном случае оно вернёт 2.99.

Уберём из шаблона компонента код вывода значения параметра premium. Теперь элемент <p>Shipping: {{ shipping }}</p>, который присутствовал в коде, с которого мы сегодня начали работу, сможет вывести сведения о стоимости доставки.


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

Текст Shipping: Free появляется на странице из-за того, что компоненту передан входной параметр premium, установленный в значение true.

Замечательно! Теперь мы научились передавать данные от родительских сущностей дочерним и смогли воспользоваться этими данными в компоненте для управления стоимостью доставки товаров.

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

Практикум


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

<ul><li v-for="detail in details">{{ detail }}</li></ul>

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

Вот решение задачи.

Итоги


Сегодня состоялось ваше первое знакомство с компонентами Vue. Вот что вы узнали:

  • Компоненты это блоки кода, представленные в виде пользовательских элементов.
  • Компоненты упрощают управление приложением благодаря тому, что позволяют разделить его на части, подходящие для многократного использования. Они содержат в себе описания визуальной составляющей и функционала соответствующей части приложения.
  • Данные компонента представлены методом data() объекта с опциями.
  • Для передачи данных от родительских сущностей дочерним сущностям используются входные параметры (props).
  • Мы можем описать требования к входным параметрам, которые принимает компонент.
  • Входные параметры передаются компонентам через пользовательские атрибуты.
  • Данные родительского компонента можно динамически привязать к пользовательским атрибутам.
  • Инструменты разработчика Vue дают ценные сведения о компонентах.


Пользуетесь ли вы инструментами разработчика Vue?

Vue.js для начинающих, урок 1: экземпляр Vue
Vue.js для начинающих, урок 2: привязка атрибутов
Vue.js для начинающих, урок 3: условный рендеринг
Vue.js для начинающих, урок 4: рендеринг списков
Vue.js для начинающих, урок 5: обработка событий
Vue.js для начинающих, урок 6: привязка классов и стилей
Vue.js для начинающих, урок 7: вычисляемые свойства

Подробнее..

Перевод Vue.js для начинающих, урок 9 пользовательские события

04.08.2020 16:15:07 | Автор: admin
На предыдущем уроке нашего курса по Vue вы узнали о том, как создавать компоненты, и о том, как передавать данные от родительских сущностей дочерним с использованием механизма входных параметров (props). А что если данные нужно передавать в обратном направлении? Сегодня, в девятом уроке, вы узнаете о том, как наладить двустороннюю связь между компонентами разного уровня.



Vue.js для начинающих, урок 1: экземпляр Vue
Vue.js для начинающих, урок 2: привязка атрибутов
Vue.js для начинающих, урок 3: условный рендеринг
Vue.js для начинающих, урок 4: рендеринг списков
Vue.js для начинающих, урок 5: обработка событий
Vue.js для начинающих, урок 6: привязка классов и стилей
Vue.js для начинающих, урок 7: вычисляемые свойства
Vue.js для начинающих, урок 8: компоненты

Цель урока


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

Начальный вариант кода


В файле index.html учебного проекта сейчас находится такой код:

<div id="app"><product :premium="premium"></product></div>

Вот содержимое файла main.js:

Vue.component('product', {props: {premium: {type: Boolean,required: true}},template: `<div class="product"><div class="product-image"><img :src="image" /></div><div class="product-info"><h1>{{ title }}</h1><p v-if="inStock">In stock</p><p v-else>Out of Stock</p><p>Shipping: {{ shipping }}</p><ul><li v-for="detail in details">{{ detail }}</li></ul><divclass="color-box"v-for="(variant, index) in variants":key="variant.variantId":style="{ backgroundColor: variant.variantColor }"@mouseover="updateProduct(index)"></div><buttonv-on:click="addToCart":disabled="!inStock":class="{ disabledButton: !inStock }">Add to cart</button><div class="cart"><p>Cart({{ cart }})</p></div></div></div>`,data() {return {product: 'Socks',brand: 'Vue Mastery',selectedVariant: 0,details: ['80% cotton', '20% polyester', 'Gender-neutral'],variants: [{variantId: 2234,variantColor: 'green',variantImage: './assets/vmSocks-green.jpg',variantQuantity: 10},{variantId: 2235,variantColor: 'blue',variantImage: './assets/vmSocks-blue.jpg',variantQuantity: 0}],cart: 0,}},methods: {addToCart() {this.cart += 1;},updateProduct(index) {this.selectedVariant = index;console.log(index);}},computed: {title() {return this.brand + ' ' + this.product;},image() {return this.variants[this.selectedVariant].variantImage;},inStock() {return this.variants[this.selectedVariant].variantQuantity;},shipping() {if (this.premium) {return "Free";} else {return 2.99}}}})var app = new Vue({el: '#app',data: {premium: true}})

Задача


Теперь, когда product представлен самостоятельным компонентом, то, что код, имеющий отношение к корзине, находится в product, смысла не имеет. Если у каждого товара будет своя корзина, за состоянием которой нам нужно наблюдать, в нашем приложении заведётся большой беспорядок. Вместо этого мы хотели бы, чтобы корзина существовала бы на уровне корневого экземпляра Vue. Так же нам нужно, чтобы компонент product сообщал бы корневому экземпляру Vue о добавлении товаров в корзину, то есть о нажатиях на кнопку Add to cart.

Решение


Переместим данные, имеющие отношение к корзине, обратно в корневой экземпляр Vue:

var app = new Vue({el: '#app',data: {premium: true,cart: 0}})

Далее перенесём шаблон корзины обратно в index.html, приведя его код к такому виду:

<div id="app"><div class="cart"><p>Cart({{ cart }})</p></div><product :premium="premium"></product></div>

Теперь, если открыть страницу приложения в браузере и нажать на кнопку Add to cart, ничего, как и ожидается, не произойдёт.


Нажатие на кнопку Add to cart пока ни к чему не приводит

А что должно происходить по нажатию на эту кнопку? Нам нужно, чтобы при нажатии на неё корневой экземпляр Vue получал бы уведомление, которое вызывало бы метод, приводящий корзину в актуальное состояние, то есть, обновляющий значение, которое хранится в cart.

Для того чтобы этого добиться, давайте сначала перепишем код метода addToCart компонента product.

Сейчас он выглядит так:

addToCart() {this.cart += 1;},

Приведём его к такому виду:

addToCart() {this.$emit('add-to-cart');},

Что всё это значит?

А значит это вот что. Когда вызывается метод addToCart, генерируется пользовательское событие с именем add-to-cart. Другими словами когда нажимают на кнопку Add to cart, вызывается метод, генерирующий событие, сообщающее о том, что только что была нажата кнопка (то есть что только что произошло событие, вызванное нажатием кнопки).

Но прямо сейчас ничто в приложении не ожидает появления этого события, не прослушивает его. Давайте добавим прослушиватель событий в index.html:

<product :premium="premium" @add-to-cart="updateCart"></product>

Тут мы пользуемся конструкцией вида @add-to-card аналогично тому, как пользуемся конструкцией :premium. Но если :premium это трубопровод, по которому можно передавать данные дочернему компоненту от родительского, то @add-to-cart можно сравнить с радиоприёмником родительского компонента, который принимает от дочернего компонента сведения о том, что была нажата кнопка Add to cart. Так как радиоприёмник находится в теге <product>, вложенном в <div id="app">, это значит, что при поступлении сведений о нажатии на Add to cart будет вызван метод updateCart, находящийся в корневом экземпляре Vue.

Код @add-to-cart=updateCart в переводе на обычный язык выглядит так: Когда услышишь, что произошло событие add-to-cart, вызови метод updateCart.

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

methods: {updateCart() {this.cart += 1;}}

Собственно говоря, это точно такой же метод, который использовался раньше в product. Но теперь он находится в корневом экземпляре Vue и вызывается по нажатию на кнопку Add to cart.


Кнопка снова работает

При нажатии на кнопку, находящуюся в компоненте product, вызывается метод addToCart, который генерирует событие. Корневой экземпляр Vue, слушая радио, узнаёт о том, что данное событие произошло и вызывает метод updateCart, который увеличивает число, хранящееся в cart.

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

Данные, передаваемые в событии, можно описать в виде второго аргумента, передаваемого $emit в коде метода addToCart компонента product:

this.$emit('add-to-cart', this.variants[this.selectedVariant].variantId);

Теперь в событии передаётся идентификатор (variantId) товара, который пользователь хочет добавить в корзину. Это значит, что мы, вместо того чтобы просто увеличивать количество товара в корзине, можем пойти дальше и хранить в корзине более подробные сведения о добавленных в неё товарах. Для этого сначала преобразуем корзину в массив, записав пустой массив в cart:

cart: []

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

methods: {updateCart(id) {this.cart.push(id);}}

После однократного нажатия на кнопку в массив попадает идентификатор товара. Массив выводится на странице.


Массив с идентификатором товара выводится на странице

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

<p>Cart({{ cart.length }})</p>


На странице выводятся сведения о количестве товаров, добавленных в корзину

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

Практикум


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

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

Итоги


Вот что вы сегодня узнали:

  • Компонент может сообщать родительской сущности о том, что в нём что-то произошло, пользуясь конструкцией $emit.
  • Родительский компонент может использовать обработчик события, заданный с использованием директивы v-on (или её сокращённой версии @), для организации реакции на события, генерируемые дочерними компонентами. Если событие происходит в родительском компоненте может быть вызван обработчик события.
  • Родительский компонент может пользоваться данными, переданными в событии, сгенерированном дочерним компонентом.

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

Vue.js для начинающих, урок 1: экземпляр Vue
Vue.js для начинающих, урок 2: привязка атрибутов
Vue.js для начинающих, урок 3: условный рендеринг
Vue.js для начинающих, урок 4: рендеринг списков
Vue.js для начинающих, урок 5: обработка событий
Vue.js для начинающих, урок 6: привязка классов и стилей
Vue.js для начинающих, урок 7: вычисляемые свойства
Vue.js для начинающих, урок 8: компоненты

Подробнее..

Перевод Vue.js для начинающих, урок 11 вкладки, глобальная шина событий

11.08.2020 16:15:49 | Автор: admin
Сегодня, в 11 уроке, который завершает этот учебный курс по основам Vue, мы поговорим о том, как организовать содержимое страницы приложения с помощью вкладок. Здесь же мы обсудим глобальную шину событий простой механизм по передаче данных внутри приложения.



Vue.js для начинающих, урок 1: экземпляр Vue
Vue.js для начинающих, урок 2: привязка атрибутов
Vue.js для начинающих, урок 3: условный рендеринг
Vue.js для начинающих, урок 4: рендеринг списков
Vue.js для начинающих, урок 5: обработка событий
Vue.js для начинающих, урок 6: привязка классов и стилей
Vue.js для начинающих, урок 7: вычисляемые свойства
Vue.js для начинающих, урок 8: компоненты
Vue.js для начинающих, урок 9: пользовательские события
Vue.js для начинающих, урок 10: формы

Цель урока


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

Начальный вариант кода


Вот как на данном этапе работы выглядит содержимое файла index.html:

<div id="app"><div class="cart"><p>Cart({{ cart.length }})</p></div><product :premium="premium" @add-to-cart="updateCart"></product></div>

В main.js имеется следующий код:

Vue.component('product', {props: {premium: {type: Boolean,required: true}},template: `<div class="product"><div class="product-image"><img :src="image" /></div><div class="product-info"><h1>{{ title }}</h1><p v-if="inStock">In stock</p><p v-else>Out of Stock</p><p>Shipping: {{ shipping }}</p><ul><li v-for="(detail, index) in details" :key="index">{{ detail }}</li></ul><divclass="color-box"v-for="(variant, index) in variants":key="variant.variantId":style="{ backgroundColor: variant.variantColor }"@mouseover="updateProduct(index)"></div><button@click="addToCart":disabled="!inStock":class="{ disabledButton: !inStock }">Add to cart</button></div><div><h2><font color="#3AC1EF">Reviews</font></h2><p v-if="!reviews.length">There are no reviews yet.</p><ul><li v-for="review in reviews"><p>{{ review.name }}</p><p>Rating: {{ review.rating }}</p><p>{{ review.review }}</p></li></ul></div><product-review @review-submitted="addReview"></product-review></div>`,data() {return {product: 'Socks',brand: 'Vue Mastery',selectedVariant: 0,details: ['80% cotton', '20% polyester', 'Gender-neutral'],variants: [{variantId: 2234,variantColor: 'green',variantImage: './assets/vmSocks-green.jpg',variantQuantity: 10},{variantId: 2235,variantColor: 'blue',variantImage: './assets/vmSocks-blue.jpg',variantQuantity: 0}],reviews: []}},methods: {addToCart() {this.$emit('add-to-cart', this.variants[this.selectedVariant].variantId);},updateProduct(index) {this.selectedVariant = index;},addReview(productReview) {this.reviews.push(productReview)}},computed: {title() {return this.brand + ' ' + this.product;},image() {return this.variants[this.selectedVariant].variantImage;},inStock() {return this.variants[this.selectedVariant].variantQuantity;},shipping() {if (this.premium) {return "Free";} else {return 2.99}}}})Vue.component('product-review', {template: `<form class="review-form" @submit.prevent="onSubmit"><p v-if="errors.length"><b>Please correct the following error(s):</b><ul><li v-for="error in errors">{{ error }}</li></ul></p><p><label for="name">Name:</label><input id="name" v-model="name"></p><p><label for="review">Review:</label><textarea id="review" v-model="review"></textarea></p><p><label for="rating">Rating:</label><select id="rating" v-model.number="rating"><option>5</option><option>4</option><option>3</option><option>2</option><option>1</option></select></p><p><input type="submit" value="Submit"></p></form>`,data() {return {name: null,review: null,rating: null,errors: []}},methods: {onSubmit() {if(this.name && this.review && this.rating) {let productReview = {name: this.name,review: this.review,rating: this.rating}this.$emit('review-submitted', productReview)this.name = nullthis.review = nullthis.rating = null} else {if(!this.name) this.errors.push("Name required.")if(!this.review) this.errors.push("Review required.")if(!this.rating) this.errors.push("Rating required.")}}}})var app = new Vue({el: '#app',data: {premium: true,cart: []},methods: {updateCart(id) {this.cart.push(id);}}})

Вот как сейчас выглядит приложение.


Страница приложения

Задача


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

Решение задачи


Для того чтобы решить нашу задачу, мы можем добавить на страницу систему вкладок. Одна из них, с заголовком Reviews, будет выводить отзывы. Вторая, с заголовком Make a Review, будет выводить форму для отправки отзывов.

Создание компонента, реализующего систему вкладок


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

Vue.component('product-tabs', {template: `<div><span class="tab" v-for="(tab, index) in tabs" :key="index">{{ tab }}</span></div>`,data() {return {tabs: ['Reviews', 'Make a Review']}}})

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

В данных компонента имеется массив tabs, содержащий строки, которые мы используем в качестве заголовков вкладок. В шаблоне компонента применяется конструкция v-for, с помощью которой для каждого элемента массива tabs создаётся элемент <span>, содержащий соответствующую строку. То, что формирует этот компонент на данном этапе работы над ним, будет выглядеть так, как показано ниже.


Компонент product-tabs на начальном этапе работы над ним

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

@click="selectedTab = tab"

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

То есть, если пользователь щёлкнет по вкладке Reviews, то в selectedTab будет записана строка Reviews. Если будет сделан щелчок по вкладке Make a Review, то в selectedTab попадёт строка Make a Review.

Вот как теперь будет выглядеть полный код компонента.

Vue.component('product-tabs', {template: `<div><ul><span class="tab"v-for="(tab, index) in tabs"@click="selectedTab = tab">{{ tab }}</span></ul></div>`,data() {return {tabs: ['Reviews', 'Make a Review'],selectedTab: 'Reviews' // устанавливается с помощью @click}}})

Привязка класса к активной вкладке


Пользователь, работая с интерфейсом, в котором используются вкладки, должен знать о том, какая вкладка является активной. Реализовать подобный механизм можно, воспользовавшись привязкой классов к элементам <span>, использующимся для вывода названий вкладок:

:class="{ activeTab: selectedTab === tab }"

Вот CSS-файл, в котором определён стиль использованного здесь класса activeTab. Вот как выглядит этот стиль:

.activeTab {color: #16C0B0;text-decoration: underline;}

А вот стиль класса tab:

.tab {margin-left: 20px;cursor: pointer;}

Если объяснить вышеприведённую конструкцию простым языком, то оказывается, что к вкладке применяется стиль, заданный для класса activeTab, в том случае, когда selectedTab равняется tab. Так как в selectedTab записывается название вкладки, по которой только что щёлкнул пользователь, стиль .activeTab будет применяться именно к активной вкладке.

Другими словами, когда пользователь щёлкнет по первой вкладке, в tab будет находиться Reviews, то же самое будет записано и в selectedTab. В результате к первой вкладке будет применён стиль .activeTab.

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


Выделенный заголовок активной вкладки

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

Работа над шаблоном компонента


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

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

template: `<div><ul><span class="tab":class="{ activeTab: selectedTab === tab }"v-for="(tab, index) in tabs"@click="selectedTab = tab">{{ tab }}</span></ul><div><p v-if="!reviews.length">There are no reviews yet.</p><ul><li v-for="review in reviews"><p>{{ review.name }}</p><p>Rating: {{ review.rating }}</p><p>{{ review.review }}</p></li></ul></div></div>`

Обратите внимание на то, что мы избавились от тега <h2><font color="#3AC1EF">, так как нам больше не нужно выводить заголовок Reviews над списком отзывов. Вместо этого заголовка будет выводиться заголовок соответствующей вкладки.

Но одного только переноса кода шаблона недостаточно для того чтобы обеспечить вывод отзывов. Массив reviews, данные которого используются для вывода отзывов, хранится в составе данных компонента product. Нам нужно передать этот массив в компонент product-tabs, используя механизм входных параметров компонента. Добавим в объект с опциями, используемый при создании product-tabs, следующее:

props: {reviews: {type: Array,required: false}}

Передадим массив reviews из компонента product в компонент product-tabs, воспользовавшись, в шаблоне product, следующей конструкцией:

<product-tabs :reviews="reviews"></product-tabs>

А теперь поразмыслим о том, что нужно вывести на странице в том случае, если пользователь щёлкнет по заголовку вкладки Make a Review. Это, конечно, форма для отправки отзывов. Для того чтобы подготовить проект к дальнейшей работе над ним перенесём код подключения компонента product-review из шаблона компонента product в шаблон product-tabs. Разместим следующий код ниже элемента <div>, используемого для вывода списка отзывов:

<div><product-review @review-submitted="addReview"></product-review></div>

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


Промежуточный этап работы над страницей

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

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


Теперь, когда мы подготовили основные элементы шаблона компонента product-tabs, пришло время создать систему, которая позволит выводить разные элементы страницы основываясь на том, по заголовку какой именно вкладки щёлкнул пользователь.

В данных компонента уже есть свойство selectedTab. Мы можем воспользоваться им в директиве v-show для организации условного рендеринга того, что относится к каждой из вкладок.

Так, к тегу <div>, содержащему код формирования списка отзывов, мы можем добавить такую конструкцию:

v-show="selectedTab === 'Reviews'"

Благодаря ей список отзывов будет выводиться тогда, когда будет активной вкладка Reviews.

Аналогично, к тегу <div>, в котором содержится код подключения компонента product-review, мы добавим следующее:

v-show="selectedTab === 'Make a Review'"

Это приведёт к тому, что форма будет выводиться только тогда, когда активна вкладка Make a Review.

Вот как теперь будет выглядеть шаблон компонента product-tabs:

template: `<div><ul><span class="tab":class="{ activeTab: selectedTab === tab }"v-for="(tab, index) in tabs"@click="selectedTab = tab">{{ tab }}</span></ul><div v-show="selectedTab === 'Reviews'"><p v-if="!reviews.length">There are no reviews yet.</p><ul><li v-for="review in reviews"><p>{{ review.name }}</p><p>Rating: {{ review.rating }}</p><p>{{ review.review }}</p></li></ul></div><div v-show="selectedTab === 'Make a Review'"><product-review @review-submitted="addReview"></product-review></div></div>`

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


Щелчки по вкладкам приводят к скрытию одних элементов и к отображению других

Но отправка отзывов с помощью формы всё так же не работает. Исследуем проблему и исправим её.

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


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


Предупреждение в консоли

Очевидно, система не может обнаружить метод addReview. Что с ним случилось?

Для ответа на этот вопрос вспомним о том, что addReview это метод, который объявлен в компоненте product. Он должен вызываться в том случае, если компонент product-review (а это дочерний компонент компонента product) генерирует событие review-submitted:

<product-review @review-submitted="addReview"></product-review>

Именно так всё и работало до переноса вышеприведённого фрагмента кода в компонент product-tabs. А теперь дочерним компонентом product является компонент product-tabs, а product-review это теперь не ребёнок, компонента product, а его внук.

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

Рефакторинг кода проекта


Для того чтобы обеспечить связь внучатых компонентов с их бабушками и дедушками, или для того, чтобы наладить связь между компонентами одного уровня, нередко используют механизм, называемый глобальной шиной событий (global event bus).

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

var eventBus = new Vue()

Этот код попадёт на верхний уровень файла main.js.

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

Сейчас в компоненте product-review, в методе onSubmit, есть такая строчка:

this.$emit('review-submitted', productReview)

Заменим её на следующую, воспользовавшись eventBus вместо this:

eventBus.$emit('review-submitted', productReview)

После этого больше не нужно прослушивать событие review-submitted компонента product-review. Поэтому изменим код этого компонента в шаблоне компонента product-tabs на такой:

<product-review></product-review>

Из компонента product теперь можно удалить метод addReview. Вместо него мы воспользуемся следующей конструкцией:

eventBus.$on('review-submitted', productReview => {this.reviews.push(productReview)})

О том, как именно применить её в компоненте, мы поговорим ниже, а пока в двух словах опишем то, что в ней происходит. Эта конструкция указывает на то, что когда eventBus генерирует событие review-submitted, нужно взять данные, передаваемые в этом событии (то есть productReview) и поместить их в массив reviews компонента product. Собственно говоря, это очень похоже на то, что до сих пор делалось в методе addReview, который нам больше не нужен. Обратите внимание на то, что в вышеприведённом фрагменте кода используется стрелочная функция. Этот момент достоин более подробного освещения.

Причины использования стрелочной функции


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

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

eventBus.$on('review-submitted', function (productReview) {this.reviews.push(productReview)}.bind(this))

Завершение работы над проектом


Мы почти достигли цели. Всё, что осталось сделать найти место для фрагмента кода, обеспечивающего реакцию на событие review-submitted. Таким местом в компоненте product может стать функция mounted:

mounted() {eventBus.$on('review-submitted', productReview => {this.reviews.push(productReview)})}

Что это за функция? Это хук жизненного цикла, который вызывается один раз после того, как компонент будет смонтирован в DOM. Теперь, после того, как компонент product будет смонтирован, он будет ожидать появления событий review-submitted. После того, как такое событие будет сгенерировано, в данные компонента будет добавлено то, что передано в этом событии, то есть productReview.

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


Форма работает так, как нужно

Шина событий это не лучшее решение для обеспечения связи компонентов


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

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

Практикум


Добавьте в проект вкладки Shipping и Details, на которых, соответственно, выводится стоимость доставки покупок и сведения о товарах.

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

Итоги


Вот что вы узнали, изучив этот урок:

  • Для организации механизма вкладок можно воспользоваться средствами условного рендеринга.
  • Для передачи данных между компонентами приложений часто пользуются глобальной шиной событий, представленной отдельным экземпляром Vue, создаваемым без использования объекта с опциями.
  • Шина событий это не самое лучшее решение задачи организации обмена данными между компонентами. Для решения подобной задачи лучше всего воспользоваться специализированной библиотекой. Например Vuex.

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

Если вы только что завершили этот курс просим поделиться впечатлениями.

Vue.js для начинающих, урок 1: экземпляр Vue
Vue.js для начинающих, урок 2: привязка атрибутов
Vue.js для начинающих, урок 3: условный рендеринг
Vue.js для начинающих, урок 4: рендеринг списков
Vue.js для начинающих, урок 5: обработка событий
Vue.js для начинающих, урок 6: привязка классов и стилей
Vue.js для начинающих, урок 7: вычисляемые свойства
Vue.js для начинающих, урок 8: компоненты
Vue.js для начинающих, урок 9: пользовательские события
Vue.js для начинающих, урок 10: формы

Подробнее..

Перевод 5 библиотек для Vue.js, без которых мне не обойтись

06.09.2020 16:13:36 | Автор: admin
Опытные разработчики знают о том, что иногда, пытаясь сэкономить время и решить какие-то задачи своего проекта с помощью пакета, созданного кем-то другим, можно, в итоге, потратить больше времени, чем было сэкономлено. Библиотеки, жёстко регламентирующие реализацию неких механизмов и не позволяющие решать с их помощью необычные задачи, выходящие за рамки того, что кажется правильным их авторам, заставляют нас, буквально сразу же после их установки, жалеть о том, что мы вообще решили их попробовать.



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

1. Скрытие элементов по щелчку за их пределами


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

Это библиотека vue-clickaway.


Скрытие элемента по щелчку за его пределами

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


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

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

directives: { onClickaway }

Но не так:

components: { onClickaway }

Вот как сделать директиву доступной на глобальном уровне (в main.js):

import { directive as onClickaway } from 'vue-clickaway'Vue.directive('on-clickaway', onClickaway)

Вот как пользоваться этой директивой в шаблоне (тут приведён, для простоты, сокращённый вариант кода):

<buttonv-on-clickaway="closeYearSelect"class="select_option gray"@click="toggleYearSelect"><span class="txt">{{ selectedYear }}</span><span class="arrow blind">Open</span></button>

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

v-on-clickaway="closeMethodName"

Обратите внимание на то, что vue-clickaway всегда нужно использовать с методом, который что-то закрывает, а не с методом, который отображает и скрывает элемент. Я имеют в виду то, что метод, подключённый к v-on-clickaway, должен выглядеть примерно так:

closeMethod() {this.showSomething = false}

Но этот метод не должен быть таким:

toggleMethod() {this.showSomething = !this.showSomething}

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

2. Всплывающие уведомления


Создавать всплывающие уведомления можно с использованием различных инструментов, но я большой фанат библиотеки vue-toastification.


Уведомление, реализованное с помощью vue-toastification

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

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


Библиотеку vue-toastification можно использовать разными способами. Подробности об этом ищите в её документации. Так, её можно применять на уровне компонента, на глобальному уровне, или даже вместе с Vuex, в том случае, если уведомления нужно показывать, основываясь на изменениях состояния приложения, или на действиях, связанных с сервером.

Вот пример глобального использования этой библиотеки (в main.js):

import Toast from "vue-toastification"// Стили уведомленийimport "vue-toastification/dist/index.css"Vue.use(Toast, {transition: "Vue-Toastification__bounce",maxToasts: 3,newestOnTop: true,position: "top-right",timeout: 2000,closeOnClick: true,pauseOnFocusLoss: true,pauseOnHover: false,draggable: true,draggablePercent: 0.7,showCloseButtonOnHover: false,hideProgressBar: true,closeButton: "button",icon: true,rtl: false,})

Стилями уведомлений можно управлять по-отдельности, задавая их в каждом компоненте, но в вышеприведённом случае я сделал стили уведомлений доступными во всём приложении, импортировав их в main.js. После этого я настроил параметры уведомлений. Это избавило меня от необходимости писать один и тот же код каждый раз, когда мне нужно воспользоваться уведомлением. У библиотеки vue-toastification имеется отличная площадка для экспериментов. На ней можно увидеть результаты воздействия параметров на уведомления и тут же скопировать в свой код то, что нужно. Именно так я поступил и в вышеприведённом примере.

Рассмотрим пару вариантов использования этой библиотеки.

Вариант 1: использование уведомлений в компоненте (в шаблоне)


<button @click="showToast">Show toast</button>

Вот метод, вызываемый при щелчке по кнопке:

methods: {showToast() {this.$toast.success("I'm a toast!")}}

Вариант 2: вывод уведомления при возникновении ошибки (или при успешном выполнении операции) в Vuex


Вот пример кода, демонстрирующий использование this._vm.$toast.error в Vuex при возникновении ошибки:

async fetchSomeData({ commit }) {const resource_uri = `/endpoint/url`try {await axios.get(resource_uri).then((response) => {commit(SET_DATA, response.data)})} catch (err) {this._vm.$toast.error('Error message: ' + err.message)}}

Изменить тип уведомления можно, просто поменяв имя метода .error на .success, .info или .warning. А если надо можно и вовсе это убрать и получить уведомление с настройками, задаваемыми по умолчанию.

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

3. Работа с таблицами


Таблицы это очень важная часть многих веб-приложений. Если выбрать не очень качественную библиотеку для работы с таблицами, это может принести немало проблем. Я испытал множество подобных библиотек и остановился на vue-good-table.


Пример использования vue-good-table

Я уверен в том, что эта библиотека способна решить большинство задач по работе таблицами, встающими перед разработчиком. И её название, good-table (хорошая таблица), это не просто слова. Это действительно хорошая библиотека, которая предоставляет нам гораздо больше возможностей, чем можно ожидать.

Использование vue-good-table


В следующем примере я привязываю данные :rows к геттеру Vuex, называемому getOrderHistory:

<vue-good-tableclass="mx-4":columns="columns":rows="getOrderHistory":search-options="{ enabled: true }":pagination-options="{enabled: true,mode: 'records',perPage: 25,position: 'top',perPageDropdown: [25, 50, 100],dropdownAllowAll: false,setCurrentPage: 2,nextLabel: 'next',prevLabel: 'prev',rowsPerPageLabel: 'Rows per page',ofLabel: 'of',pageLabel: 'page', // для режима 'pages'allLabel: 'All'}">

Вот описания столбцов таблицы в локальных данных (data()):

columns: [{label: 'Order Date',field: 'orderDtime',type: 'date',dateInputFormat: 'yyyy-MM-dd HH:mm:ss',dateOutputFormat: 'yyyy-MM-dd HH:mm:ss',tdClass: 'force-text-center resizeFont'},{label: 'Order Number',field: 'orderGoodsCd',type: 'text',tdClass: 'resizeFont'},{label: 'Title',field: 'orderTitle',type: 'text',tdClass: 'resizeFont ellipsis'},{label: 'Price',field: 'saleAmt',type: 'number',formatFn: this.toLocale},{label: 'Edit btn',field: 'deliveryUpdateYn',type: 'button',tdClass: 'force-text-center',sortable: false},]

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

В вышеприведённом примере я использую некоторые возможности vue-good-table по настройке таблиц. Это, например, установка входного и выходного формата дат (это позволяет мне брать полное описание даты и времени с сервера и показывать эти сведения пользователям в более удобном формате). Я, кроме того, использую тут formatFn для форматирования цены с помощью особого метода, который я назвал toLocale. Затем я настраиваю внешний вид ячеек таблицы, привязывая tdClass к классам, которые я задал в моём локальном теге <style>. Пакет vue-good-table, на самом деле, обладает бесконечными встроенными возможностями по настройке. Эти возможности позволяют создавать таблицы, которые способны подойти для весьма широкого спектра вариантов их применения.

Создание собственных шаблонов


Библиотека vue-good-table отлично работает и с шаблонами, созданными программистами самостоятельно. Это значит, что в ячейки таблиц можно легко внедрять кнопки, поля для выбора значений из списка, или что угодно другое. Для того чтобы это сделать, достаточно, пользуясь директивой v-if, указать место, в котором должно быть расположено что-то особенное. Вот пример добавления кнопки в столбец таблицы.

<template slot="table-row" slot-scope="props"><span v-if="props.column.field === 'cancelYn' && props.row.cancelYn === 'Y' "><BaseButton class="button" @click="showModal">Cancel Order</BaseButton></span></template>

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

Для того чтобы добавить в таблицу ещё один собственный столбец, достаточно просто добавить v-else-if после закрывающего тега конструкции, в которой использовалась директива v-if (в нашем случае это тег <span>). После этого надо описать логику нового столбца. В целом, можно сказать, что библиотека vue-good-table способна помочь в любой ситуации, связанной с выводом таблиц на веб-страницах.

4. Средство для выбора дат


А теперь расскажу о библиотеке, предназначенной для организации выбора дат. Нечто подобное встречается в огромном множестве реально существующих приложений. Библиотек, реализующих этот функционал, существует гораздо больше, чем библиотек для решения других задач, о которых я тут рассказываю. Я, решая эту задачу, регулярно возвращаюсь к библиотеке vue2-datepicker.


Пример использования библиотеки vue2-datepicker

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

Обратите внимание на то, что хотя соответствующий пакет и называется vue2-datepicker, у вас не должно возникнуть проблем при его использовании в приложениях, основанных на Vue 3 (то же самое касается и других библиотек, рассмотренных в этом материале).

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


Библиотеку можно импортировать в компонент или включить в шаблон.

Вот пример её импорта в компонент:

import DatePicker from 'vue2-datepicker';// стилиimport 'vue2-datepicker/index.css';

Вот как пользоваться ей в шаблоне:

<date-pickerv-model="dateRange"value-type="format"range@clear="resetList"@input="searchDate"></date-picker>

Здесь я использую опцию range, позволяя пользователю выбирать диапазоны дат. Здесь же я, с помощью директивы v-model, подключаю данные, введённые пользователям, к значению dateRange. Затем dateRange может, например, использоваться vue-good-table для настройки вывода данных в таблице. Я, кроме того, использую тут опции событий @clear и @input, применяя их для вызова методов, один из которых сбрасывает таблицу (resetList), а второй отправляет запрос на сервер (searchDate). Библиотека vue2-datepicker предлагает нам гораздо больше опций и событий, чем я тут описал. Но тем, о чём я тут рассказал, я пользуюсь чаще всего.

5. Рейтинги


Системы рейтингов можно найти, например, на таких сайтах, как Amazon и Rotten Tomatoes. Возможно, подобными системами вы пользуетесь и не в каждом проекте, но я, на каждом сайте, где это нужно, реализую данную возможность с использованием библиотеки vue-star-rating.


Пример использования библиотеки vue-star-rating

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

Оценку, которую пользователь поставил чему-то с помощью элемента управления из vue-star-rating, можно легко, используя v-model, передать туда, где планируется её использовать. Оценки можно делать изменяемыми или неизменяемыми, воспользовавшись единственным параметром.

А если окажется, что вам при работе с этой библиотекой понадобится больше возможностей взгляните на пакет vue-rate-it того же автора.

Использование vue-star-rating


Вот как пользоваться этой библиотекой в шаблоне (с опциями):

<star-ratingclass="star-rating":rating="newReivew.score"active-color="#FBE116":star-size="starSize":increment="increment":show-rating="showRating"@rating-selected="setRating"/>

Вот как импортировать её в компонент:

import StarRating from 'vue-star-rating'export default {components: {StarRating},}

Итоги


Мы рассмотрели 5 библиотек для Vue. Надеемся, вы нашли здесь что-то такое, что пригодится вам при разработке ваших проектов.

Какими библиотеками для Vue.js вы пользуетесь чаще всего?

Подробнее..

Best practices для клиент-серверного проекта PoC

22.12.2020 12:11:10 | Автор: admin
image
Типичный проект PoC (Proof of Concept) состоит из клиента с GUI, сервера c бизнес логикой и API между ними. Также используется база данных, хранящая оперативную информацию и данные пользователей. Во многих случаях необходима связь с внешними системами со своим API.
Когда у меня возникла необходимость в создании проекта PoC, и я начал разбираться в деталях, то оказалось, что порог вхождения в веб-программирование весьма высок. В крупных проектах для каждого компонента есть выделенные специалисты: front-end, back-end разработчики, UX/UI дизайнеры, архитекторы баз данных, специалисты по API и информационной безопасности, системные администраторы. В небольшом PoC надо самому во всем разобраться, выбрать подходящее техническое решение, реализовать и развернуть. Ситуацию ухудшает тот факт, что из обучающих материалов не всегда понятно, почему предлагается сделать именно так, а не иначе, есть ли альтернативы, является ли решение best practice или это частное мнение автора. Поэтому я разработал заготовку под названием Common Test DB, отвечающую лучшим практикам. Ее можно использовать для начала любого проекта, остается только наполнить функциональным смыслом.
В статье я подробно опишу примененные best practices, расскажу про имеющиеся альтернативы и в конце размещу ссылки на исходники и работающий в сети пример.

Требования к проекту PoC


Начнем с минимальных требований к проекту PoC. В статье Блокчейн: что нам стоит PoC построить? я уже рассматривал из каких частей состоит универсальная информационная система. В этой статье подробно разберем состав типичного проекта PoC. Даже минимальный проект в клиент-серверной архитектуре требует значительной функциональности и технической проработки:

Клиент:
  • Должен осуществлять запросы к серверу, используя REST API
  • Регистрировать, авторизовать, аутентифицировать пользователей
  • Принимать, разбирать и визуализировать пришедшие данные

Сервер
  • Должен принимать HTTP/HTTPS запросы от клиента
  • Осуществлять взаимодействие с внешними системами
  • Взаимодействовать с базой данных (реализовать базовую функциональности CRUD)
  • Обеспечивать безопасность данных, вызовов API и конфигурационных параметров
  • Осуществлять логирование и мониторинг работоспособности

База данных
  • Необходимо реализовать структуру DB
  • Наполнить DB начальными значениями

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

Технологический стек


  • В настоящее время для большинства веб проектов используетсяязык JavaScript.Над JavaScript есть надстройкаTypeScript, которая обеспечивает типизацию переменных и реализацию шаблонов.
  • Для полнофункциональных веб проектов есть несколько стандартных стеков технологий. Один из самых популярных: MEVN (MongoDB + Express.js + Vue.js + Node.js), мне больше нравится PEVN (PostgreSQL + Express.js + Vue.js + Node.js), т.к. RDB базы мне технологически ближе, чем NoSQL.
  • Для GUI существует несколькофреймворков (Vue, React, Angular). Тут выбор, скорее, определяется традициями или личными предпочтениями, поэтому сложно говорить, что лучше, а что хуже. Сначала я выбрал Vue.js, да так на нем и остался. Этот фреймворк хорошо развивается, для него есть готовыевизуальныекомпоненты встилеMaterial Design (Vuetify), которые хорошо смотрятся даже при непрофессиональном применении. Считается, что для React порог вхождения выше, чем для Vue. В React сначала надо осознать специфичную объектную модель, которая отличается от классического веб программирования: компоненты как функции, компоненты как классы, цепочки вызовов.
  • Выбор базы данных уже сложнее, чем личные предпочтения. Но для PoC, зачастую, требуется не функциональность или производительность, а простота. Поэтому в примере будем использовать самую простую в мире базу SQLite. В промышленных PoC в качестве SQL базы я использую PostgreSQL, в качестве NoSQL ее же, т.к. запас прочности уPostgreSQL огромен. Прочность, конечно, не бесконечна и когда-нибудь настанет необходимость перехода на специализированную базу, но это отдельная тема для обсуждения.

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

Сервер


HTTP сервер


В качестве HTTP сервера япробовал два варианта:

Express более зрелый, Fastify, говорят, более быстрый. Есть еще варианты серверов, примеры перечислены в статье The Best 10 Node.js Frameworks. Я использовал самый популярный Express.js.

HTTPS запросы


Говоря про HTTP, я всегда подразумеваю HTTPS, т.к. без него крайне сложно построить безопасное взаимодействие между клиентом и сервером. Для поддержки HTTPS надо:
  • Получить SSL (TLS) сертификат
  • На сервере реализовать поддержку HTTPS протокола
  • На клиенте использовать запросы к клиенту с префиксом HTTPS

Получаем сертификат
Для организации работы HTTPS протокола нужен SSL сертификат. Существуют бесплатные сертификаты Lets Encrypt, но солидные сайты получают платные сертификаты от доверительных центров сертификации CA(Certificate Authority). Для наших тестовых целей на локальном хосте достаточно, так называемого, self-signedcertificate. Он генерируется с помощью утилиты openssl:
openssl req -nodes -new -x509 -keyout server.key -out server.crt

Далее отвечаем на несколько вопросов про страну, город, имя, email и т.п. В результате получаемдва файла:
  • server.key ключ
  • server.crt сертификат

Их нужно будет подложить нашему Express серверу.

HTTPS сервер
С Express сервером все достаточно просто, можно взять стандартный пакет HTTPS из Node.js, и использовать официальную инструкцию: How to create an https server?
После реализации HTTPS сервера используем полученные файлы server.key и server.crt и стандартный для HTTPS порт443.

Взаимодействие с внешними системами


Самая известная библиотека для реализации HTTP/HTTPS запросов: Axios. Она используется как в сервере для вызова API внешних систем, так и в клиенте для вызова API сервера. Есть еще варианты библиотек, которые можно использовать дляспецифичных целей: Обзор пяти HTTP-библиотек для веб-разработки.

Взаимодействие с базой данных


Для работы с базой данных я использовал самую популярную библиотеку для Node.js: Sequelize. В ней мне больше всего нравится то, что переключиться между различными типами баз данных можно всего лишь изменив несколько настроечных параметров. При условии, конечно, что в самом коде не используется специфика определенной базы.Например, чтобы переключиться с SQLight на PostgreSQL, надо в конфигурационном файле сервера заменить:
dialect: 'sqlite'

на
dialect: 'postgres'

И при необходимости изменить имя базы, пользователя и хост.
В PoC я использовал базу данныхSQLite, которая не требует установки.Для администрирования баз данных есть хорошо развитые GUI:

Безопасность доступа к данным


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

  • Получить данные могут все зарегистрированные пользователи.
  • Изменить и удалить данные может только владелец этих данных.

Такая модель доступа к данным называетсяDiscretionary Access Control (DAC), по-русски: избирательное управление доступом. В этой модели владелец данных может делать с ними любые CRUD операции, а права остальных пользователей ограниченны.
Существуют несколько моделей контроля доступа к данным. ПомимоDiscretionary Access Control (DAC)OWASP рекомендует (Access Control Cheat Sheet) использовать следующие:
  • Role-Based Access Control (RBAC)
  • Mandatory Access Control (MAC)
  • Permission Based Access Control

Безопасность вызовов API


В моей предыдущей статье я рассматривал все возможные угрозы: Безопасность REST API от А до ПИ.В PoC реализованы следующие механизмы:
  • Cross-Site Request Forgery (CSRF) (Межсайтовая подмена запросов)

От данной угрозы реализован механизм CSRF токенов когда для каждой сессии пользователя генерируется новый токен (он же SessionId) и сервер проверяет его валидность при любых запросах с клиента. Алгоритм генерации и проверки SessionId подробно описан далее в разделе Авторизация, аутентификация пользователей.
  • Cross-origin resource sharing (CORS) (Кросс-доменное использование ресурсов)

Защита реализована с помощью пакета CORS. В config файле сервера указываются адрес (origin), с которого могут поступать API запросы и список методов (methods), которые может использовать клиент. В дальнейшем сервер будет автоматически ограничивать прием запросов в соответствии с этими настройками.
Для защиты от угроз, перечисленных далее, я использовал пакетhelmet, который позволяет задать значения для определенных HTTP заголовков:
  • Cross-site Scripting (XSS) (Межсайтовое выполнение скриптов)

Для защиты выставляется заголовок, ограничивающий выполнение скриптов:
X-XSS-Protection: 1; mode=block

  • Insecure HTTP Headers

Блокируется отправка сервером заголовка, дающего дополнительную информацию злоумышленнику:
X-Powered-By: Express

  • Insecure HTTP Headers: HTTP Strict Transport Security (HSTS)

Используется Strict-Transport-Security заголовок, который запрещает браузеру обращаться к ресурсам по HTTP протоколу, только HTTPS:
Strict-Transport-Security: max-age=15552000; includeSubDomains

  • Insecure HTTP Headers: X-Frame-Options (защита от Clickjacking)

Данный заголовок позволяет защититься от атаки Clickjacking. Он разрешает использовать фреймы только в нашем домене:
X-Frame-Options: SAMEORIGIN

  • Insecure HTTP Headers: X-Content-Type-Options

Установка заголовка X-Content-Type-Options запрещает браузеру самому интерпретировать тип присланных файлов и принуждает использовать только тот, что был прислан в заголовке Content-Type:
X-Content-Type-Options: nosniff

Защита от DoS


Необходимо защитить сервер и от отказа в обслуживании (DoS-атаки). Например, ограничить число запросов от одного пользователя или по одному ресурсу в течении определенного времени.
Для Node.js существуют средства, позволяющие автоматически реализовывать ограничения на число запросови сразу посылать ответ 429 Too Many Requests, не нагружая бизнес логику сервера.
В примере реализовано простейшее ограничение на число запросов от каждого пользователя в течении определенного времени по таблице Data. В промышленных системах надо защищать все запросы, т.к. даже один запрос, оставшийся без проверки может дать возможность провести атаку.

Безопасность конфигурационных параметров


Настройки для клиента и сервера хранятся в файлах configв JSON формате. Чувствительные настройки сервера нельзя хранить вconfig файлах, т.к. они могут стать доступны в системах версионного контроля. Для такой информации как: логины/пароли для базы данных, ключи доступа к API и т.д. надо использовать специализированные механизмы:
  • Задавать эти параметры в .env файле. Файл .env прописан в gitignore и не сохраняется в системах версионного контроля, поэтомупоэтому туда можно безопасно записывать чувствительную информацию.
  • Использовать механизм переменных окружения (environment variables), которые устанавливаются вручную или прописываютсяинсталляторами.

Логирование


Стандартные требования к логированию:
  • Обеспечить разный уровень логирования (Debug; Info; Warning; Error и т.д.)
  • Логировать HTTP/HTTPS запросы
  • Дублировать консольный вывод в файлы на диске
  • Обеспечить самоочистку файлов логирования по времени иобъёму

Варианты пакетов для логирования, которые я пробовал:

В проекте я использовал log4js. Пакет log4jsработает интуитивно понятно и достаточно развит, чтобы реализовать все требования.

Мониторинг


Мы не будем останавливаться на внешних система мониторинга: Обзор систем мониторинга серверов, т.к. они начинают играть важную роль не на этапе PoC, а в промышленной эксплуатации.
Обратимся к внутренним системам, которые позволяют наглядно посмотреть, что сейчас происходит с сервером, например пакетexpress-status-monitor. Если его установить, топо endpoint
/monitor

можно наглядно мониторить простейшие параметры работы сервера, такие как загрузка CPU, потребление памяти, http нагрузку и т.п. Приведу скриншот из нашего примера. На скриншоте горит красная плашка Failed и может показаться, что что-то не так. Но, на самом деле, все в порядке, т.к. вызов API сознательно делается на несуществующий endpoint:
/wrongRoutePath

image

База данных


Структурабазы данных


В нашем примере реализованы две таблицы с говорящими именами:
  • Users
  • Data

Лучшие практики говорят, что все проверки надо помаксимуму отдавать базе данных. Поэтому в схеме базы данных аккуратно определяем все типы данных, добавляем ограничения (constraints) и внешние ключи.Для пакета sequelize данная функциональность подробно описана в стандартной документации.
В нашем пример сделан один внешний ключ UserId в таблице Data для того, чтобы гарантировать, что у каждой записи в таблицеData будет определенный владелец User. Это позволит при изменении или удалении записи в Data реализовать проверку пользователя, т.к. по нашей логике данные может удалять только их владелец.
Еще одна особенность нашей схемы в том, что один столбец таблицы Data задан в формате JSON. Такой формат удобно использовать, если внешняя система возвращает данные в JSON. В этом случае можно просто записать полученные данные, не тратя усилия на парсинг,а потом делать SQL запросы, используя расширенный синтаксис.Описание работы со столбцами JSON можно найти в официальной документации баз данных. В качестве независимого описания мне понравилась статья на Хабре, в которой описаны все варианты запросов: "JSONB запросы в PostgreSQL.

Формальная схема DB
Cхема таблицыUsers:
  • id INTEGERPRIMARY KEY AUTOINCREMENT уникальный ID
  • uuid UUID NOT NULL UNIQUE уникальный UUID, по нему идет связь с данными этого пользователя в таблице Data
  • email VARCHAR(255) NOT NULL UNIQUE
  • password VARCHAR(64)
  • ddosFirstRequest DATETIME
  • ddosLastRequest DATETIME
  • ddosRequestsNumber DECIMAL
  • lastLogin DATETIME
  • loginState VARCHAR(255) NOT NULL
  • sessionId VARCHAR(1024)
  • commonToken VARCHAR(1024)
  • googleToken VARCHAR(1024)
  • googleAccessToken VARCHAR(1024)
  • googleRefreshToken VARCHAR(1024)
  • createdAt DATETIME NOT NULL
  • updatedAt DATETIME NOT NULL

Схема таблицыData:
  • id INTEGERPRIMARY KEY AUTOINCREMENT
  • uuid UUID NOT NULL UNIQUE
  • ownerUuid UUID NOT NULL
  • data JSON NOT NULL
  • UserId INTEGER REFERENCES Users (id) ON DELETE CASCADE ON UPDATE CASCADE,
  • createdAt DATETIME NOT NULL
  • updatedAt DATETIME NOT NULL


Начальные значения


  • В таблице Users задан один пользовательuser@example.comс паролем password.
  • В таблице Data записано несколько предопределенных значений, которые можно получить из GUI.

Далее перейдем к клиенту.

Клиент


HTTPS запросы


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

RESTful API


REST API лучше реализовывать, используя стандартные средства, чтобы было проще тестировать и документировать, например SwaggerHub
Наше REST API сформировано по правилам Open API и использует стандартные HTTP методы для манипуляции объектами из таблиц Users и Data.В схеме нашего API не все сделано по чистым правилам REST, но главное соблюдена общая концепция.
Хорошей практикой являетсяограничение объёма возвращаемых данных. Для этого используются параметры в запросе: фильтр (filter), ограничение (limit) и смещение (offset). В примере этого нет, но в промышленных системах эта функциональность должна быть реализована.Но даже если реализованы ограничивающие механизмы на клиенте, должна осуществляться дополнительная проверка на сервере на максимальные значения. В нашем примере в конфигурации сервера реализован ограничитель на максимальное число возвращаемых строк из базы,который подставляется вSELECT запросы:limit: 1000

YAML файл с Open API описанием
---swagger: "2.0"info:  description: "This is a common-test-db API. You can find out more about common-test-db\    \ at \nhttps://github.com/AlexeySushkov/common-test-db\n"  version: "2.0.0"  title: "common-test-db"  contact:    email: "alexey.p.sushkov@gmail.com"  license:    name: "MIT License"    url: "https://github.com/AlexeySushkov/common-test-db/blob/main/LICENSE"host: "localhost:443"basePath: "/commontest/v1"tags:- name: "data"  description: "Everything about Data"  externalDocs:    description: "Find out more"    url: "https://github.com/AlexeySushkov/common-test-db/"- name: "users"  description: "Everything about Users"  externalDocs:    description: "Find out more"    url: "https://github.com/AlexeySushkov/common-test-db/"schemes:- "https"paths:  /data:    get:      tags:      - "data"      summary: "Gets all data"      description: "Gets all data"      operationId: "getData"      produces:      - "application/json"      parameters: []      responses:        "200":          description: "successful operation"          schema:            type: "array"            items:              $ref: "#/definitions/Data"        "400":          description: "Invalid status value"      x-swagger-router-controller: "Data"    post:      tags:      - "data"      summary: "Add a new data to the db"      operationId: "addData"      consumes:      - "application/json"      produces:      - "application/json"      parameters:      - in: "body"        name: "body"        description: "Data object that needs to be added to the db"        required: true        schema:          $ref: "#/definitions/Data"      responses:        "500":          description: "Create data error"      x-swagger-router-controller: "Data"    put:      tags:      - "data"      summary: "Update an existing data"      operationId: "updateData"      consumes:      - "application/json"      produces:      - "application/json"      parameters:      - in: "body"        name: "body"        description: "Data information that needs to be changed"        required: true        schema:          $ref: "#/definitions/Data"      responses:        "400":          description: "Invalid uuid"        "404":          description: "User not found"        "500":          description: "Update data error"      x-swagger-router-controller: "Data"    delete:      tags:      - "data"      summary: "Delete an existing data"      operationId: "deleteData"      consumes:      - "application/x-www-form-urlencoded"      - "application/json"      produces:      - "application/json"      parameters:      - name: "uuid"        in: "formData"        description: "uuid"        required: true        type: "string"      responses:        "400":          description: "Invalid uuid"        "404":          description: "User not found"        "500":          description: "Delete data error"      x-swagger-router-controller: "Data"  /users:    post:      tags:      - "users"      summary: "Register new user"      operationId: "userRegister"      consumes:      - "application/json"      produces:      - "application/json"      parameters:      - in: "body"        name: "body"        description: "User that needs to be deleted"        required: true        schema:          $ref: "#/definitions/User"      responses:        "500":          description: "Register user error"      x-swagger-router-controller: "Users"    put:      tags:      - "users"      summary: "Update existing user"      operationId: "userUpdate"      consumes:      - "application/json"      produces:      - "application/json"      parameters:      - in: "body"        name: "body"        description: "User information that needs to be changed"        required: true        schema:          $ref: "#/definitions/User"      responses:        "404":          description: "User not found"        "500":          description: "Delete user error"      x-swagger-router-controller: "Users"    delete:      tags:      - "users"      summary: "Delete user"      operationId: "userDelete"      consumes:      - "application/json"      produces:      - "application/json"      parameters:      - in: "body"        name: "body"        description: "User that needs to be added to the db"        required: true        schema:          $ref: "#/definitions/User"      responses:        "404":          description: "User not found"        "500":          description: "Delete user error"      x-swagger-router-controller: "Users"  /users/login:    post:      tags:      - "users"      summary: "Login"      operationId: "userLogin"      consumes:      - "application/json"      produces:      - "application/json"      parameters:      - in: "body"        name: "body"        description: "User that needs to be added to the db"        required: true        schema:          $ref: "#/definitions/User"      responses:        "404":          description: "User not found"        "500":          description: "Login user error"      x-swagger-router-controller: "Users"definitions:  Data:    type: "object"    required:    - "Counter1"    - "Counter2"    properties:      Counter1:        type: "integer"        format: "int64"      Counter2:        type: "integer"        format: "int64"    example:      Counter1: 10      Counter2: 20  User:    type: "object"    required:    - "email"    - "password"    properties:      email:        type: "string"      password:        type: "string"    example:      password: "password"      email: "email"  ApiResponse:    type: "object"    properties:      status:        type: "string"


Интересная возможность встроить Swagger прямо в сервер, далее подложить ему YAML, реализующий API и по endpoint:
/api-docs

получить стандартную Swagger панель управления и осуществлять тестирование и просмотр документации. Скриншот Swagger, встроенного в наш сервер:

image

GraphQL


Существуют и альтернативы REST API, например GraphQL. Данный стандарт разработан Facebook и позволяет избавиться от недостатков REST:
  • В REST API взаимодействие происходит с использованием многочисленные endpoints. В GraphQL одна.
  • В REST API для получения данных от разных endpoints необходимо осуществить множественные запросы. В GraphQL все необходимые данные можно получить одним запросом.
  • В REST API клиент не может контролировать объём и содержимое данных в ответе. В GraphQL набор возвращаемых данных предсказуем, т.к. задается в запросе. Это сразу избавляет от угрозы: Excessive Data Exposure (Разглашение конфиденциальных данных)
  • В REST API отсутствует обязательность формальной схемы. В GpaphQL создание схемы API с определенными типами данных обязательно, при этом автоматически получается документация и сервер для тестирования (самодокументироваемость и самотестируемость).
  • Клиент всегда может запросить схему API с сервера, тем самым синхронизировать формат своих запросов
  • Из коробки работают подписки, реализованные на технологии WebSockets

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

Регистрация,аутентификация и авторизация пользователей


Если приложение доступно из Интернета, то в форме регистрации имеет смысл использовать функциональность reCaptсha от Google для защиты от ботов и DoS атак. В нашем примере я использую пакет vue-recaptcha.
Для использованияreCaptсha нужно на сайте Google зарегистрировать приложение, получить sitekey и прописать его в настройках клиента. После этого на форме появится, известный всем, вопрос про робота:
image

В примере реализована регистрация пользователей с помощью логина/пароля ис использованием Google Account.
  • При регистрации по логин/пароль пароли в базе данных, разумеется, не хранятся в виде текста, а хранится только хеш. Для его генерации используется библиотекаbcrypt, которая реализует алгоритм хеширванияBlowfish. Сейчас уже есть мнение, что надо использовать более надежные алгоритмы: Password Hashing: Scrypt, Bcrypt and ARGON2.
  • При регистрации с помощью Google Account организация хранения аутентификационных данных проще, т.к. в базу записывается только token, полученный от Google.

Для реализации различных схем аутентификации есть библиотека Passport, которая упрощает работу и скрывает детали. Но чтобы понять как работают алгоритмы, в примере все сделано в соответствии со стандартами, которые я уже описывал в своей статье: Современные стандарты идентификации: OAuth 2.0, OpenID Connect, WebAuthn.
Далее в статье разберем практическую реализацию.

Аутентификация по логину/паролю (Basic Authentication)
Тут все достаточно просто. При получении запроса на логин сервер проверяет, что пользователь с присланнымemail существует и что хеш полученного пароля совпадает с хешом из базы. Далее:
  • Сервер формирует SessionId в формате UUID и Token в формате JWT (JSON Web Token)
  • Токену присваиватся время жизни, которое определяется бизнес задачами. Например, Google устанавливет своим токенам время жизни 1 час.
  • Для формирования токена используется пакетjsonwebtoken
  • В JWT помещается email пользователя, который подписывается ключом сервера. Ключ это текстовая строка, которая хранится, как конфигурационный параметр на сервере.Содержимое токена кодируется Base64
  • SessionId сохраняется в базе и отправляется клиенту в заголовке X-CSRF-Token
  • Token сохраняется в базе и отправляется клиенту в теле HTTP ответа
  • Получив SessionId клиент сохраняетSessionId в sessionStorageбраузера
  • Token записывается в localStorage или Сookiesбраузерадля реализации возможности запомнить меня на этом устройстве
  • В дальнейшем клиент будет присылать SessionId в HTTP заголовкеX-CSRF-Token:

X-CSRF-Token: 69e530cf-b641-445e-9866-b23c492ddbab

  • Token будет присылаться в заголовкеHTTP Authorization с префиксомBearer:

Authorization: BearereyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InRlc3RAdGVzdC5jb20iLCJpYXQiOjE2MDYxNTY3MTUsImV4cCI6MTYwNjc2MTUxNX0.h3br5wRYUhKIFs3SMN2ZPvMcwBxKn7GMIjJDzCLm_Bw

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

  1. Токен подписан сервером;
  2. Пользователь с email существует;
  3. Время жизни токена не истекло. Если же оно истекло, то при выполнении предыдущих условий автоматически формируется новый токен.

В общем случае излишне генерировать и SessionId и Token. В нашем PoC для примера реализованы оба механизма. Посмотрим внимательней на наш JWT. Пример закодированного JWT токена из заголовка:
Authorization:yJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InRlc3RAdGVzdC5jb20iLCJpYXQiOjE2MDYxNTY3MTUsImV4cCI6MTYwNjc2MTUxNX0.h3br5wRYUhKIFs3SMN2ZPvMcwBxKn7GMIjJDzCLm_Bw

Раскодируем его открытыми средствами. Например самый простой способ загрузить на сайтhttps://jwt.io

image

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

Аутентификация с помощью Google Account (Token-Based Authentication)
Тут посложнее, поэтому нарисую диаграмму:

image

1. Пользователь заходит на GUI PoC, выбирает Login with Google".
2. Клиент запрашивает у сервера SessionId и настройки Google. Настройки надо предварительно получить с сайта Google, например, по инструкции из моей статьи: Современные стандарты идентификации: OAuth 2.0, OpenID Connect, WebAuthn
3. Клиент сохраняет SessionId в sessionStorageбраузера
4. Из браузера формируется GET запрос на GoogleAuthorization Server (красная стрелка). Для понимания алгоритма, надо обратить внимание, что ответ на этот запрос браузер получит от Google только в самом конце call flow на шаге 13 и это будет 307 (Redirect).

Формат GET запроса:
https://accounts.google.com/o/oauth2/v2/auth?response_type=code&client_id=918876437901-g312pqdhg5bcju4hpr3efknv.apps.googleusercontent.com&scope=openid+email&redirect_uri=http://localhost:8081/redirect&access_type=offline&state=450c2fd9-0a5e-47d2-8ed5-dd1ff4670b58

В этом запросе:
  • accounts.google.com/o/oauth2/v2/auth endpoint для начала аутентификации. У Google есть, кстати, адрес, по которому можно посмотреть актуальный список всех Google API endpoints:https://accounts.google.com/.well-known/openid-configuration
  • response_type=code параметр говорит, что ожидаем получить в ответAuthorization Code
  • client_id Client ID, выданный при регистрации приложения на Google (в примере указан не настоящий)
  • scope=openid email к каким данным пользователя мы хотим получить доступ
  • redirect_uri = localhost:8081/redirectCallback адрес, заданный при регистрации приложения. В нашем случае это адрес на нашем сервере, который получит запрос от Google
  • state = SessionId, который передается между клиентом, Google и сервером для защиты от вмешательства внешнего злоумышленника

5. Google Authorization Server показывает стандартную Google форму логина.
6. Пользователь вводит свои логин/пароль от Google аккаунта.
7. Google проверяет пользователя и делает GET запрос на адрес Callback с результатом аутентификации, Authorization Code в параметре code и нашем SessionId в параметреstate:
  • state: '450c2fd9-0a5e-47d2-8ed5-dd1ff4670b58',
  • code: '4/0AY0e-g4pg_vSL1PwUWhfDYj3gpiVPUg20qMkTY93JYhmrjttedYwbH376D_BvzZGmjFdmQ',
  • scope: 'email openid www.googleapis.com/auth/userinfo.email',
  • authuser: '1',
  • prompt: 'consent'

8. Сервер, не отправляя пока ответ на GET, формирует POST запрос с Authorization Code, Client ID и Client Secret:
POSThttps://oauth2.googleapis.com/token?code=4/0AY0e-g4pg_vSL1PwUWhfDYj3gpiVPUg20qMkTY93JYhmrjttedYwbH376D_BvzZGmjFdmQ&client_id=918876437901-g312pqdhg5bcju4hpr3efknv.apps.googleusercontent.com&client_secret=SUmydv3-7ZDTIh8аНК85chTOt&grant_type=authorization_code&redirect_uri=http://localhost:8081/redirect

  • oauth2.googleapis.com/token это endpoint для получения token
  • code только что присланный code
  • client_id Client ID, выданный при регистрации приложения на Google (в примере указан не настоящий)
  • client_secret Client Secret, выданный при регистрацииприложения на Google (в примере указан не настоящий)
  • grant_type=authorization_code единственно возможное значение из стандарта

9. Google проверяет присланные данные и формирует access token в формате JWT (JSON Web Token), подписанный своим приватным ключом. В этом же JWT может содержаться и refresh token, c помощью которого возможно продолжение сессии после ее окончания:
  • access_token: 'ya29.a0AfH6SMBH70l6wUe1i_UKfjJ6JCudA_PsIIKXroYvzm_xZjQrCK-7PUPC_U-3sV06g9q7OEWcDWYTFPxoB1StTpqZueraUYVEWisBg46m1kQAtIqhEPodC-USBnKFIztGWxzxXFX47Aag',
  • expires_in: 3599,
  • refresh_token: '1//0cAa_PK6AlemYCgYIARAAGAwSNwF-L9IrdUt1gzglxh5_L4b_PwoseFlQA1XDhqte7VMzDtg',
  • scope: 'openid www.googleapis.com/auth/userinfo.email',
  • token_type: 'Bearer',
  • id_token: 'eyJhbGciOiJSUzI1NiIsImtpZCI6ImRlZGMwMTJkMDdmNTJhZWRmZDVmOTc3ODRlMWJjYmUyM2MxOTcfobDCf2VrxXb6CCxoL_dZq1WnlEjBZx_Sf6Rg_tn3x4gWtusO1oe_bJx_gvlSLxtvSOdO_kPB2uGGQHr3xzF_Evr-S-BiGS8zMuIkslyN6fU7P7BdNVyOYAIYFvHikyIpAoesV2Fd2yBSngBwGmWfrHL7Z2415UrnlCG4H1Nw'

10. Сервер валидирует пришедший Token. Для этого нужен открытый ключ от Google. Актуальные ключи находятся по определенному адресу и периодически меняются:https://www.googleapis.com/oauth2/v1/certs.Поэтому надо или получать их каждый раз по этому адресу, либо хардкодить и следить за изменениями самому.Декодируем пришедший token, для нас самое главное это подтвержденный email пользователя:
decodedIdToken: {iss: 'https://accounts.google.com',azp: '918962537901-gi8oji3qk312pqdhg5bcju4hpr3efknv.apps.googleusercontent.com',aud: '918962537901-gi8oji3qk312pqdhg5bcju4hpr3efknv.apps.googleusercontent.com',sub: '101987547227421522632',email: 'work.test.mail.222@gmail.com',email_verified: true,at_hash: 'wmbkGMnAKOnfAKtGQFpXQw',iat: 1606220748,exp: 1606224348}

11. Проверяем, что email пользователя существует, или создаём нового пользователя c email из токена. При этомSessionId и полученные от Google токены сохраняются в базе.
12. Только теперь отвечаем Google HTTP кодом 307 (Redirect) и заголовком Location c адресом на клиенте:
HTTP Location:http://localhost:8080/googleLogin

13. И только теперь Google отвечает браузеру с тем же кодом307 (Redirect) и заголовком Location с заданным нами адресом
14. Браузер переходит на адрес, указанный вLocation иклиент определяет, что произошла успешная аутентификация пользователя с помощью Google аккаунта
15. Клиент, по сохраненному в sessionStorage SessionId, получает на сервере токен и данные пользователя
16. Клиент сохраняет токен в localStorage браузера

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

Прием, обработка и визуализация данных


Сделаем стандартный вид приложения, как рекомендует Google:
  • Drawer (navigation-drawer) в левой стороне
  • Меню сверху (v-app-bar)
  • Footer внизу (v-footer)
  • Для визуализации полученных данных используем карточки (Data Cards)

image

Для создания новых данных используется простая форма ввода:

image

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

  1. Для Vue есть библиотекаVuex, реализующаяstate management pattern.
  2. Есть универсальная библиотека Redux основанная на Flux от Facebook. Она реализует концепцию состояний, используя понятия action, state, view.

  • В примере я использовал Vuex.
  • Также необходимRouter для реализации переходов между страницами.

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

Архитектура


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

Заключение


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

Исходники на Github:https://github.com/AlexeySushkov/common-test-db
Пример запущен в облаке Amazon, можно посмотреть, как он выглядит вживую:http://globalid.tech/

Остается только в канун Нового Года пожелать, чтобы сбылись все ваши самые заветные PoC!

It's only the beginning!
Подробнее..

Проблемы рендера 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. Что-то обещают множество улучшений, может, часть костылей из этой статьи даже пропадет.

Подробнее..

Онлайн-митап сообщества разработчиков MSK VUE.JS

20.07.2020 14:14:41 | Автор: admin
image

23 июля приглашаем на онлайн-митап сообщества разработчиков MSK VUE.JS.

В программе митапа:

  • Разработка конструктора отчетов c Cube.js;
  • 5 действенных техник оптимизации vue-приложений;
  • Решение проблем REST API при помощи GraphQL.

Зарегистрироваться

О митапе


Разработчик Cube.js Леонид Яковлев расскажет, как сделать конструктор отчетов при помощи cube.js на бэкенде. Подробно поговорит, что такое query builder, какие у него преимущества и покажет пример простого queryBuilder

Руководитель AFFINAGE Игорь Яковлев поделится собственными техниками, инсайтами и лайфхаками, как разрабатывать по-настоящему быстрые vue-приложения.

Разработчик Московской биржи Юлия Кузнецова расскажет, как GraphQL помогает решать проблемы REST API архитектуры и покажет, как это сделать на примере Vue Apollo.

Об экспертах


Леонид Яковлев разработчик в команде Developer Relations and Community фреймворка Cube.js.

Игорь Яковлев руководитель аутсорспродакшена по фронтенду AFFINAGE.

Юлия Кузнецова старший разработчик Московской биржи.

О MSK VUE.JS


MSK VUE.JS сообщество разработчиков Vue.js, которое регулярно проводит митапы, чтобы делиться опытом, обсуждать перспективы и строить комьюнити.

По теме:


Подробнее..

Из песочницы Переключение шаблона страниц во vuejs

04.08.2020 14:20:22 | Автор: admin

Иногда в приложении требуется шаблоны для различных страниц, чтобы не копировать код от компонента к компоненту, мы прописываем шаблон в основном компоненте (он же, обычно, App.vue) и с помощью <router-view> подставляем в него различные вьюшки.


image

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

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


Первым делом нам необходимо Vue Js приложение с подключенным роутером.


Что из себя представляют шаблоны? Правильно, обычные компоненты, которые будут вызываться в зависимости от того или иного условия, будь то активная странница, статус аутентификации или (по бреду) время суток.


Подготовим основной компонент для работы с шаблонами.

Если вы создавали проект через Vue-Cli у вас он выглядит примерно так:


//ВАШ_ПРОЕКТ/src/App.vue<template>  <div id="app">    <div id="nav">      <router-link to="/">Home</router-link> |      <router-link to="/about">About</router-link>    </div>    <router-view/>  </div></template><style>#app {  font-family: Avenir, Helvetica, Arial, sans-serif;  -webkit-font-smoothing: antialiased;  -moz-osx-font-smoothing: grayscale;  text-align: center;  color: #2c3e50;}#nav {  padding: 30px;}#nav a {  font-weight: bold;  color: #2c3e50;}#nav a.router-link-exact-active {  color: #42b983;}</style>

Нам необходимо добавить новое вычисляемое свойство (Computed) в секцию script (если у вас ее нет, скопируйте ее из любого vue компонента).

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


//ВАШ_ПРОЕКТ/src/App.vue...<script>    export default {        computed: {            //Это самое вычисляемое свойство            layout(){                //Вернем имя шаблона из роута или дефолтное значение                   //(шаблон для страниц, для которых мы не указали шаблон)                return this.$route.meta.layout || "default-layout"             }        }    }</script>...

Отредактируем секцию template добавим в нее динамический компонент, который будет меняться в зависимости от значения layout.


//ВАШ_ПРОЕКТ/src/App.vue<template>  <div id="app">      <!--Динамический компонент-->      <component :is="layout">          <router-view/>      </component>  </div></template>...

Теперь создадим пару шаблонов.

Для удобства будем хранить их в отдельной папке layouts.


скриншот структуры папкок

По значимости папка не сильно ушла от components или view, просто удобно.

Есть у Vue такой элемент slot, который позволит нам подставлять наши представления в компонент-шаблон. При отрисовке он заменяется на контент, который мы укажем при вызове компонента. Создадим три шаблона, пусть они будут одинаковыми, с разницей в цвете шапки и подвала, для наглядности.

Синий шаблон, дефолтный:



//ВАШ_ПРОЕКТ/src/layouts/Default.vue<template>    <div>        <header>            <ul class="nav">                <li><router-link class="link" to="/">Домашняя страница</router-link></li>                <li><router-link class="link" to="/page2">Страница 1</router-link></li>                <li><router-link class="link" to="/page3">Страница 2</router-link></li>                <li><router-link class="link" to="/page4">Страница 2</router-link></li>            </ul>        </header>        <section class="content">            <!--Элемент, который при отрисовке будет заменен на ваш view-->            <slot/>        </section>        <footer>        </footer>    </div></template><script>    export default {        name: "Default"    }</script><style scoped>    header{        background-color: blue;        height: 70px;        display: flex;        align-items: center;    }    footer{        background-color: blue;        height: 70px;    }    .content{        min-height: calc(100vh - 140px);    }    ul{        list-style: none;        margin: 0;        color: white;    }    li{        color: white;        display: inline;        margin: 0 5px;    }    .link{        color: white;        text-decoration: none;    }</style>

Зеленый шаблон:


//ВАШ_ПРОЕКТ/src/layouts/Green.vue<template>    <div>        <header>            <ul class="nav">                <li><router-link class="link" to="/">Домашняя страница</router-link></li>                <li><router-link class="link" to="/page2">Страница 1</router-link></li>                <li><router-link class="link" to="/page3">Страница 2</router-link></li>                <li><router-link class="link" to="/page4">Страница 2</router-link></li>            </ul>        </header>        <section class="content">            <!--Элемент, который при отрисовке будет заменен на ваш view-->            <slot/>        </section>        <footer>        </footer>    </div></template><script>    export default {        name: "green"    }</script><style scoped>    header{        background-color: green;        height: 70px;        display: flex;        align-items: center;    }    footer{        background-color: green;        height: 70px;    }    .content{        min-height: calc(100vh - 140px);    }    ul{        list-style: none;        margin: 0;        color: white;    }    li{        color: white;        display: inline;        margin: 0 5px;    }    .link{        color: white;        text-decoration: none;    }</style>

Красный шаблон:

//ВАШ_ПРОЕКТ/src/layouts/Red.vue<template>    <div>        <header>            <ul class="nav">                <li><router-link class="link" to="/">Домашняя страница</router-link></li>                <li><router-link class="link" to="/page2">Страница 1</router-link></li>                <li><router-link class="link" to="/page3">Страница 2</router-link></li>                <li><router-link class="link" to="/page4">Страница 2</router-link></li>            </ul>        </header>        <section class="content">            <!--Элемент, который при отрисовке будет заменен на ваш view-->            <slot/>        </section>        <footer>        </footer>    </div></template><script>    export default {        name: "Red"    }</script><style scoped>    header{        background-color: red;        height: 70px;        display: flex;        align-items: center;    }    footer{        background-color: red;        height: 70px;    }    .content{        min-height: calc(100vh - 140px);    }    ul{        list-style: none;        margin: 0;        color: white;    }    li{        color: white;        display: inline;        margin: 0 5px;    }    .link{        color: white;        text-decoration: none;    }</style>

Теперь зарегистрируем эти компоненты-шаблоны в нашем Vue.

//ВАШ_ПРОЕКТ/src/main.jsimport Vue from 'vue'import App from './App.vue'import router from './router'//Подключим файлы компонентовimport DefaultLayout from "./layouts/Default"import GreenLayout from "./layouts/Green"import RedLayout from "./layouts/Red"//И зарегистрируем их в нашем приложенииVue.component("default-layout", DefaultLayout)Vue.component("green-layout", GreenLayout)Vue.component("red-layout", RedLayout)Vue.config.productionTip = falsenew Vue({  router,  render: h => h(App)}).$mount('#app')

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


//ВАШ_ПРОЕКТ/src/views/page1.vue<template>    <div>        <h1>Синий шаблон</h1>    </div></template><script>    export default {        name: "Page1"    }</script><style scoped>

</style>//ВАШ_ПРОЕКТ/src/views/page2.vue<template>    <div>        <h1>Зеленый шаблон</h1>    </div></template><script>    export default {        name: "Page2"    }</script><style scoped></style>

//ВАШ_ПРОЕКТ/src/views/page3.vue<template>    <div>        <h1>Красный шаблон</h1>    </div></template><script>    export default {        name: "Page3"    }</script><style scoped></style>

//ВАШ_ПРОЕКТ/src/views/page4.vue<template>    <div>        <h1>Еще один синий шаблон</h1>    </div></template><script>    export default {        name: "Page4"    }</script><style scoped></style>

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


import Vue from 'vue'import VueRouter from 'vue-router'//Подключим наши страницыimport Page1 from "../views/Page1"import Page2 from "../views/Page2"import Page3 from "../views/Page3"import Page4 from "../views/Page4"Vue.use(VueRouter)const routes = [    {        path: '/',        name: 'Home',        component: Page1        //Так, как синий шаблон у нас является дефолтным, его можно не указывать в мета-данных    },    {        path: '/page2',        name: 'Page2',        component: Page2,        //А вот это свойство как раз будет содержать название компонента-шаблона,        //который мы хотим использовать для данной страницы        meta:{            layout: "green-layout"        }    },    {        path: '/page3',        name: 'Page3',        component: Page3,        meta:{            layout: "red-layout"        }    },    {        path: '/page4',        name: 'Page4',        component: Page4,        //И снова ничего не указываем, чтобы задействовать дефолтный шаблон    }]const router = new VueRouter({    mode: 'history',    base: process.env.BASE_URL,    routes})export default router

Запускаем наше приложение и проверяем:

image

Целиком код можно посмотреть тут
Подробнее..

Перевод Введение во Vue Storefront

11.09.2020 16:15:38 | Автор: admin

Добрый день, меня зовут Андрей Солдатов, мы в команде Россельхозбанка разрабатываем новые интересные проекты, связанные с сельским хозяйством. В качестве фронтального решения для некоторых из них мы решили использовать интересное open source решение Vue Storefront. В этой статье вы можете ознакомиться с ключевыми возможностями и особенностями этого решения. Статья является переводом статьи из официального блога Vue Storefront, оригинал доступен по ссылке.

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

Что такое Vue Storefront?

Vue Storefront это backend-независимое PWA (веб)приложение для электронной коммерции, написанное на Vue.js. То, что он использует headless-архитектуру, позволяет Vue Storefront работать с любой eCommerce платформой, он может выступать в качестве PWA для Magento, Shopify, BigCommerce, WooCommerce и других.

Это очень популярный open source проект с сильным и развивающимся сообществом.

Ключевые особенности Vue Storefront:

  • Кроссплатформенность

  • Фокус на производительность

  • Принцип mobile-first

  • Передовые технологии

  • Нет ограничений в стилистике тем и кастомизаций

  • Открытый исходный код с лицензией MIT

  • Сообщество опытных разработчиков

  • Серверный рендеринг из коробки (для SEO)

  • Offline режим

Тут вы найдете сайт Vue Storefront, а здесь GitHub репозиторий.

Как это связано с серверными платформами?

Vue Storefront является backend-независимым благодаря vue-storefront-api и отдельным API-коннекторам для eCommerce backend-платформ. Формат данных в vue-storefront-api всегда одинаков для любой платформы. Это означает, что независимо от того, какую eCommerce платформу вы используете, ваш веб-интерфейс остается без каких-либо изменений.

Это отличная стратегия, так как вы легко сможете переходить с одной платформы на другую (или с одной версии на другую, например, Magento 1 -> 2), не трогая ваш frontend.

Коннектор API работает в два этапа:

  • data pump (на рисунке mage2nosql) извлекает статические данные (каталог, заказы и т.д.) из вашей eCommerce платформы во Vue Storefront ElasticSearch и изменяет формат на тот, который используется vue-storefront-api. После получения данных вы можете отобразить ваш каталог продуктов во Vue Storefront. После загрузки данных в ElasticSearch они будут синхронизироваться с изменениями на стороне backend-платформы и обновлять контент.

  • worker pool это синхронизация так называемых динамических вызовов (пользовательских сессий, правил корзины и т.д.), которые не могут быть сохранены в базе данных и должны вызываться vue-storefront-api напрямую с backend платформы.

Управляя этими двумя этапами интеграции, Vue Storefront может работать с вашей backend платформой.

Некоторые из популярных backend платформ уже имеют готовые интеграции (Magento 2, Magento 1, CoreShop, BigCommerce, WooCommerce), но вы можете легко создать свою собственную с помощью шаблона.

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

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

При работе с Vue Storefront необходимо ознакомиться с тремя концепциями:

  • Vue Storefront Core (/core(http://personeltest.ru/aways/github.com/DivanteLtd/vue-storefront/tree/master/core)) является связующим звеном для всех его функций, которые позволяют Vue Storefront работать. Он содержит все необходимые для работы приложения: SSR, билды, встроенные библиотеки и хелперы. Не следует трогать эту папку при создании собственных решений, для того, чтобы продолжать получать обновления.

  • Vue Storefront Modules (/core/modules и /src/modules) - это eCommerce модули. Каждый модуль представляет собой одну функцию (например, корзина, wish list, каталог, сторонние интеграции). Вы можете добавлять / удалять / редактировать эти модули по своему усмотрению и использовать только те функции, которые вам нужны. Они же используются и для сторонних расширений.

  • Vue Storefront Themes (src/themes) - это реализация вашего сайта. В темах вы можете использовать и расширять всю логику из зарегистрированных модулей/ядра и добавлять свою HTML-разметку и стили. Vue Storefront из коробки предоставляет полностью надстраиваемую тему по умолчанию.

Подводя итог: ваш сайт это в основном Vue Storefront Themes, в котором используются функции, предоставляемые Vue Storefront Modules. Vue Storefront Core склеивает это все воедино.

Понимание этих трех аспектов позволит вам работать с Vue Storefront и создавать собственные сайты на нем.

Полезные материалы: структура проекта Vue Storefront.

Установка Vue Storefront

Есть три варианта:

Это все, что нужно чтобы VS заработал с нашим демо-бэкендом.Это все, что нужно чтобы VS заработал с нашим демо-бэкендом.
  • Вы можете подключить свой frontend к нашей демо backend-платформе (лучший вариант, чтобы попробовать Vue Storefront).

  • Вы можете настроить frontend с вашим собственным vue-storefront-api и базой данных, выгруженной из демо-версии.

  • Вы можете настроить frontend с помощью vue-storefront-api, подключенного к вашему eCommerce backend.

Чтобы сделать что-то из этих вариантов, просто введите "yarn installer" в корневом каталоге проекта и ответьте на вопросы в консоли. После завершения установки для запуска проекта введите команду "yarn dev" (по умолчанию на 3000 порту). Независимо от того, что вы выберете, вы сможете изменить настройки в файле конфигурации.

Конфигурационный файл Vue Storefront

Большая часть конфигурации Vue Storefront (например, активная тема, адреса внутреннего интерфейса API, настройки магазинов и т.д.) выполняется через файл конфигурации, который можно найти в папке "/config". Файл default.json содержит все настройки по умолчанию.

Для вашей реализации вы должны создать файл local.json и включить поля из default.json, которые вы хотите переопределить. Эти два файла будут объединены с приоритетом local.json в процессе сборки. Если вы используете инсталлятор для установки своего инстанса Vue Storefront, он сам сгенерирует корректные файлы конфигурации.

Создание тем в Vue Storefront

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

Бизнес-логика из основных компонент может быть легко внедрена в любые темы с использованием Vue.js миксин Бизнес-логика из основных компонент может быть легко внедрена в любые темы с использованием Vue.js миксин

Механизм внедрения основной бизнес-логики в темы очень прост. Мы используем Vue.js миксины для сохранения возможности обновления основной бизнес-логики. Таким образом, предполагая, что у нас есть, например, ядро Microcart с такой бизнес-логикой (слева на рисунке), мы можем легко внедрить его в любой из наших компонентов темы (справа), просто импортировав его и добавив в качестве mixins: [Microcart]. Это все, что вам нужно, чтобы использовать основной бизнес-логику в вашей теме. При таком подходе мы можем легко обновлять все основные компоненты, не ломая сайт.

Самый простой способ создать свою тему - это создать копию темы по умолчанию, изменить её имя в package.json, выбрать активную тему в config/local.json и запустить yarn, чтобы сделать Lerna линкование (которое используются для monorepos).

Автономный режим и кеш

Vue Storefront продолжает работать, даже когда пользователь не в сети. Нам удалось это сделать с помощью активного использования кеша браузера.

  • Для статик ресурсов (только для prod) мы используем плагин sw-precache (конфигурацию можно найти в /core/build/webpack.prod.sw.config.js). Они кэшируются в Service Worker и могут быть проверены на вкладке браузера Application.

Здесь вы можете найти статик ресурсы. Обратите внимание, что Service Worker работают только в prod режиме.Здесь вы можете найти статик ресурсы. Обратите внимание, что Service Worker работают только в prod режиме.
  • Для кэша каталога и данных мы используем IndexedDB и Local Storage. Мы также предварительно выбираем товары из посещенных категорий, поэтому, как только вы посетите одну из них, все ее продукты будут доступны вам в автономном режиме. Механизм автономного кэширования находится в папке /core/lin./storage.

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

Что еще?

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

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

Видео с обучением

Также вы можете посмотреть видео с 4го Vue Storefront хакатона с ознакомительным обучением.

Подробнее..

Перевод Создание блога с помощью Nuxt Content(часть первая)

11.10.2020 18:05:36 | Автор: admin

Создание блога на Nuxt Content


От переводчика: Я собирался сделать собственную статью по Nuxt Content, но наткнулся на готовую статью, которая отлично раскрывает тему. Лучше у меня вряд ли получится, поэтому я решил перевести. Написал автору в твиттер и практически сразу получил согласие. Статья будет с моими дополнениями для лучшего понимания темы.



Модуль Content в Nuxt это headless CMS основанной на git файловой системе, которая предоставляет мощные функции для создания блогов, документации или просто добавления контента на обычный сайт. В этой статье мы разберем большинство преимуществ этого модуля и узнаем как создать блог с его помощью.


Видео обзор готового проекта:


Your browser does not support HTML5 video.

Посмотреть Демо /
Код проекта



Начало работы


Установка


Чтобы начать работу с модулем Content, нам сначала нужно установить модуль с помощью npm или yarn.


yarn add @nuxt/content

npm install @nuxt/content

Затем мы добавим его в сборку модулей в файле nuxt.config.


export default {  modules: ['@nuxt/content']}

Если вы создаете новый проект с помощью create-nuxt-app, можете выбрать опцию добавить модуль Content, и он будет установлен.

Создаем страницу


Модуль Content читает файлы в нашем каталоге content/.


mkdir content

Если вы создали свой проект с помощью create-nuxt-app, каталог content/ будет уже создан.

Давайте создадим директорию articles/, куда мы сможем добавлять статьи для нашего блога.


mkdir content/articles

Модуль Content может анализировать markdown, csv, yaml, json, json5 или xml файлы. Давайте создадим нашу первую статью в markdown файле:


touch content/articles/my-first-blog-post.md

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


# My first blog postWelcome to my first blog post using content module

В markdown мы создаем заголовок <h1> с помощью значка #. Убедитесь, что вы оставили пробел между ним и заголовком вашего блога. Для получения дополнительной информации о записи в markdown стиле смотрите Руководство по основному синтаксису.

Отображение контента


Чтобы отобразить контент на странице, мы используем динамическую страницу, добавив к странице знак подчеркивания (_). Создав компонент страницы с именем _slug.vue внутри папки blog, мы можем использовать переменную params.slug, предоставляемую vue router, для получения имени каждой статьи.


touch pages/blog/_slug.vue

Затем используем asyncData в компоненте страницы для получения содержимого статьи до того, как страница будет отрисована. Мы можем получить доступ к контенту через context, используя переменную $content. Поскольку мы хотим получить динамическую страницу, нам также необходимо знать, какую статью нужно получить с помощью params.slug, который доступен нам через context.


<script>  export default {    async asyncData({ $content, params }) {      // fetch our article here    }  }</script>

Внутри асинхронной функции asyncData мы создаем переменную с именем article, которая принимает контент, используя await, за которым следует $content. Нужно передать в $content параметры того, что мы хотим получить, в нашем случае это папка articles и slag, который мы получаем из params. По цепочке в конце добавляем метод fetch, который возвращает нужную статью.


<script>  export default {    async asyncData({ $content, params }) {      const article = await $content('articles', params.slug).fetch()      return { article }    }  }</script>

Чтобы отобразить контент, используем компонент <nuxt-content />, передав переменную в параметр document. В этом примере мы заключили его в HTML тег article, согласно правилам семантического синтаксиса, но вы можете использовать div или другой тег HTML, если хотите.


<template>  <article>    <nuxt-content :document="article" />  </article></template>

Теперь мы можем запустить сервер разработки и перейти по маршруту http://localhost:3000/blog/my-first-blog-post. Мы должны увидеть контент из .md файла.


статья из файла my-first-blog-post.md


Введенные переменные по умолчанию


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


  • body: содержимое документа
  • dir: директория
  • extension: расширение файла (.md в этом примере)
  • path: путь к файлу
  • slug: имя файла
  • toc: массив, содержащий оглавление
  • createdAt: дата создания файла
  • updatedAt: дата последнего изменения файла

Мы можем получить доступ ко всем этим переменным, используя созданную ранее переменную article. Article это объект, который содержит все эти дополнительные введенные переменные, к которым у нас есть доступ. Давайте проверим их, распечатав с помощью тега <pre>.


<pre> {{ article }} </pre>

Теперь на нашей странице мы видим, что у нас есть объект с переменной, которая представляет собой пустой массив, и переменную содержимого(body), которая включает все наши теги h1 и p, а также некоторую другую информацию, которую мы рассмотрим позже. Если мы прокрутим вниз, вы увидите все остальные переменные, к которым есть доступ.


"dir": "/articles","path": "/articles/my-first-blog-post","extension": ".md","slug": "my-first-blog-post","createdAt": "2020-06-22T10:58:51.640Z","updatedAt": "2020-06-22T10:59:27.863Z"

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


<p>Post last updated: {{ article.updatedAt }}</p>

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


methods: {    formatDate(date) {      const options = { year: 'numeric', month: 'long', day: 'numeric' }      return new Date(date).toLocaleDateString('en', options)    } }

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


<p>Article last updated: {{ formatDate(article.updatedAt) }}</p>

Пользовательские введенные переменные


Мы также можем добавить пользовательские введенные переменные, добавив блок YAML в наш md файл. Он должен находиться в верхней части файла, иметь допустимый формат YAML и находиться между двумя тройными пунктирными линиями. Это полезно для добавления переменных SEO, таких как заголовок, описание и изображение вашей статьи.


---title: My first Blog Postdescription: Learning how to use @nuxt/content to create a blogimg: first-blog-post.jpgalt: my first blog post---

Теперь у нас есть переменные title, description, img и alt, к которым у нас есть доступ из объекта article`.


<template>  <article>    <h1>{{ article.title }}</h1>    <p>{{ article.description }}</p>    <img      :src="article.image"      :alt="article.alt"    />    <p>Article last updated: {{ formatDate(article.updatedAt) }}</p>    <nuxt-content :document="article" />  </article></template>

Чтобы отрендерить изображения, включенные в YAML разделе файла, нам нужно либо поместить их в статическую папку, либо использовать синтаксис:
:src="require(`~/assets/images/${article.image}`)".
Изображения, включенные в содержимое статьи, всегда следует помещать в папку static, поскольку @nuxt/content не зависит от Webpack. Эта папка не пропускается через Webpack, в отличие от папки assets.

Стилизация markdown контента


Если мы посмотрим на код получившейся страницы, мы увидим, что все, что написано внутри нашего файла, заключено в div с классом nuxt-content. Это означает, что мы можем легко добавить стили ко всем нашим элементам из нашего markdown файла, заключив их в класс nuxt-content.


<style>  .nuxt-content h2 {    font-weight: bold;    font-size: 28px;  }  .nuxt-content h3 {    font-weight: bold;    font-size: 22px;  }  .nuxt-content p {    margin-bottom: 20px;  }</style>

Чтобы использовать стили с ограниченной областью видимости с классом nuxt-content, вам необходимо использовать deep селектор: /deep/, ::v-deep или >>>


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


Наши теги из md файла преобразуются в правильные теги, что означает, что теперь у нас есть два заголовка, два тега <h1>. Удалим один из md файла.


Добавление иконки к ссылке наших заголовков


Обратите внимание, что внутри тега <h2> есть тег <a> с href, который содержит id для ссылки на себя, и тег span внутри него с icon и icon-link классы. Это полезно для ссылки на этот раздел страницы. Ссылки в заголовках пусты и поэтому скрыты, поэтому давайте добавим им стиль. Используя классы значков, мы можем добавить svg-иконки в качестве фонового изображения для нашего значка. Сначала вам нужно будет добавить сами иконки в папку с ресурсами assets. В этом примере я добавила его в папку svg и взяла иконки Steve Schoger's Hero Icons.


.icon.icon-link {  background-image: url('~assets/svg/icon-hashtag.svg');  display: inline-block;  width: 20px;  height: 20px;  background-size: 20px 20px;}

Добавляем оглавление


Сгенерированная переменная toc позволяет нам добавить оглавление к нашему посту в блоге. Давайте добавим заголовки к нашему сообщению в блоге.


## This is a headingThis is some more info## This is another headingThis is some more info

Теперь мы можем видеть эти новые заголовки внутри массива toc с идентификатором, глубиной и текстом. Значение глубины является значением тега заголовка, поэтому значение глубины 2 приравнено тегу <h2> и равно 2, значение 3 тегу<h3> и т. д.


## This is a headingThis is some more info### This is a sub headingThis is some more info### This is another sub headingThis is some more info## This is another headingThis is some more info

Поскольку у нас есть доступ к toc и тексту, мы можем перебрать и отобразить их все, а в компоненте <NuxtLink> сделать ссылку на якорь раздела, на который мы хотим создать ссылку.


<nav>  <ul>    <li v-for="link of article.toc" :key="link.id">      <NuxtLink :to="`#${link.id}`">{{ link.text }}</NuxtLink>    </li>  </ul></nav>

Теперь ссылки ToC работают, и нажатие на любую из них приведет нас к нужной части документа. Модуль Content автоматически добавляет идентификатор и ссылку к каждому заголовку. Если мы проверим один из заголовков из нашего .md файла в инструментах разработки браузера, мы увидим, что у нашего тега <h2> есть идентификатор. Это тот же идентификатор, который находится в toc, который по сути из него и берется для ссылки на правильный заголовок.


Мы можем улучшить верстку дальше, используя динамические классы для стилизации классов заголовков в зависимости от глубины заголовка, которую мы можем добавить в наш тег nuxt-link. Если ссылка имеет глубину 2, добавьте отступ по оси y, а если глубина равна 3, добавьте поле слева и отступ внизу. Здесь мы используем классы TailwindCSS, но, конечно же, можно использовать собственные имена и стили классов.


:class="{ 'py-2': link.depth === 2, 'ml-2 pb-2': link.depth === 3 }"

Использование HTML в .md файлах


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


<div class="bg-blue-500 text-white p-4 mb-4">  This is HTML inside markdown that has a class of note</div>

Добавление Vue компонента


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


Теперь мы можем добавлять компоненты в наше приложение, установив для свойства components значение true в нашем файле nuxt.config. (начиная с v2.13)


export default {  components: true}

Автоматический импорт компонентов не будет работать для <nuxt-content>, если мы не зарегистрируем их глобально, добавив глобальную папку внутри папки компонентов.


mkdir components/global

А теперь можно создать наш компонент InfoBox внутри этой папки.


<template>  <div class="bg-blue-500 text-white p-4 mb-4">    <p><slot name="info-box">default</slot></p>  </div></template>

Теперь в нашей разметке эти компоненты будут доступны без необходимости их импорта.


<info-box>  <template #info-box>    This is a vue component inside markdown using slots  </template></info-box>

Глобальные компоненты будут доступны для всего нашего приложения, поэтому будьте осторожны при добавлении компонентов в эту папку. Это работает иначе, чем добавление компонентов в папку components, которые добавляются (наверное, имеется в виду импортируются прим. пер.) только в том случае, если они используются (начиная с Nuxt v2.13 компоненты в папке components импортируются автоматически, достаточно написать в Nuxt конфиге: components: true прим. пер.).



От переводчика: На этом первая часть статьи подошла к концу. Дебби познакомила нас с мощным инструментом от создателей Nuxt'а, который они, кстати, сами используют на своем сайте для документации фреймворка. В этом, в общем-то, основное его применение. Если вы думали, что наконец-то нашли простую CMS, на которой можно быстро шлепать проекты и отдавать заказчику, то это не так. Из-за git-подобной системы наполнения контента, проект должен всегда быть под контролем разработчиков. Она идеальна для документации инструментов разработки, и любого другого контента не требующего частого обновления. Если же контент должен динамически обновляться из-за действий пользователей, то тут без базы данных не обойтись.


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


Продолжение следует...

Подробнее..

Перевод Какой будет новая версия 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-индустрии, который может серьёзно повлиять на успех компаний в будущем.

Подробнее..

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. Для более глубокого изучения советую посетить сайт этой замечательной библиотеки. Документация все еще далека от совершенства, но она регулярно обновляется как и сама библиотека.

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

Подробнее..

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.js

31.08.2020 00:13:55 | Автор: admin

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



Введение


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


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


  1. Приложение имеет множество однотипных форм, где декомпозиция компонентов не оказывает должного эффекта, а использование copy & paste только ухудшает поддерживаемость;
  2. Приложение очень динамично развивается и требуется экономить время на реализации каждой новой фичи продукта в ущерб UX (User eXperience);
  3. Приложение находится на стадии прототипирования и большая часть функциональности будет изменена или удалена в ближайшее время.

Далее необходимо определиться со списком требований к библиотеке генератора форм (данный список может отличаться от ваших требований):


  1. Библиотека для Vue.js;
  2. Поддержка Element UI, желательно актуальной версии;
  3. Построение форм из JSON schema, с использованием валидаторов;
  4. Возможность локализации форм, включая ошибки валидации.

Требования из данного списка не выглядят какими-то невыполнимыми, но дьявол кроется в деталях и, для того чтобы сделать осознанный выбор, не достаточно просто прочитать описание на сайте разработчика или README проекта. Основные проблемы можно обнаружить только при решении практических задач, а данная статья призвана облегчить жизнь тем, что автор уже прошёл по ровно уложенным граблям и готов поделиться с вами своими объективными и субъективными заключениями.


Несколько слов перед тем, как перейти к списку кандидатов. Под другие фреймворки, например, React или Angular, есть более достойные проекты, но изначально выбор пал на Vue.js, в силу его простоты встраивания и минималистичности, поэтому автор рассматривает только данный набор библиотек, а остальные фреймворки оставим за скобками этой статьи. Element очень функциональный UI kit, предоставляющий практически все примитивы для быстрой разработки веб-приложений и требующий минимального опыта от разработчика, что идеально подходит для людей, переходящих в веб-разработку из других языков программирования.


Кандидаты


vue-json-schema


Описание


  • Оригинальный репозиторий (2.0.0-alpha.2): github и npm;
  • Fork проекта (для legacy версии 1.1.1): github и npm;
  • Реализация темы для Element UI: github, демо.

Интересный проект, имеет ~360 звёзд на github, но заброшен с 2018 года, хорошо интегрирован с Element UI, позволяет расширять возможности основной библиотеки, добавляя свои собственные типы объектов.


Минусы


  • Стабильно работает только версия 1.1.1, а 2.0.0 находится на стадии готовности alpha и её использование в production версиях приложений не рекомендуется;
  • Не поддерживает механизма локализации из коробки, нужно создавать несколько схем или патчить схему на лету до передачи генератору;
  • Давно не поддерживается, последняя тестированная версия Vue.js 2.2.0 в рамках интеграции.

Плюсы


  • Возможность создавать формы из оригинальной JSON schema и применять стандартные валидаторы к данным;
  • Поддержка Element UI.

ncform


Описание



Китайский проект, имеет ~900 звёзд на github и развивается неспешно. Все комментарии в коде на китайском языке, что усложняет понимание. Имеет встроенный механизм локализации. Основная часть проекта покрыта Unit и функциональными тестами на cypress. В библиотеке используются проприетарные ключи, отступающие от оригинальной JSON schema, но при этом не ломающие её.


Минусы


  • Недостаточно качественная документация и примеры;
  • Проприетарные ключи в JSON schema;
  • Недостаточно конфигурационных возможностей UI controls.

Плюсы


  • Популярность проекта;
  • Встроенная поддержка локализации;
  • Возможность создания собственных валидаторов в JSON schema через dx-выражения;
  • Минимально необходимая поддержка Element UI.

vue-form-generator


Описание



Самый популярный проект по количеству звёзд на github ~2500, имеет хорошую документацию на английском языке. Большое количество конфигурационных возможностей элементов, но отсутствуют простые способы для локализации.


Минусы


  • Нет возможности загрузки из JSON файла, как минимум валидаторы это объекты;
  • Собственный формат схемы для создания формы, несовместим с оригинальной JSON schema;
  • Не поддерживает пакет i18n и локализацию для валидаторов;
  • Необходимо переопределять стили для ошибок валидаторов и объектов форм.

Плюсы


  • Популярность проекта;
  • Большое количество расширений;
  • Поддержка Element UI.

vue-form-json-schema


Описание



Библиотека использует классические аннотации JSON schema для валидации, но для отрисовки UI требует отдельного объекта, содержащего UI конфигурацию uiSchema. Поддерживает локализацию для ошибок валидации через проект и другие плагины для ajv, например, ajv-errors. Позволяет добавлять свои визуальные компоненты, используемые в uiSchema.


Минусы


  • Не поддерживает Element UI, требуется отдельно применять стили;
  • Очень сложно создавать uiSchema, соизмеримо с описанием визуальной схемы в блоке template vue-компонента.

Плюсы


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

vue-ele-form


Описание



Китайский проект с относительно большим количеством звёзд на github ~530, но с полностью отсутствующей документацией на английском языке. Присутствует онлайн demo и проект для визуального создания форм, онлайн.


Минусы


  • Вся документация, примеры и комментарии в коде только на китайском языке;
  • Используется отличная от оригинальной JSON schema структура описания.

Плюсы


  • Проект активно развивается и есть надежда, что он станет интернациональным;
  • Присутствует функциональность расширения данной библиотеки и уже существуют более 10 новых визуальных компонентов;
  • Поддержка Element UI.

form-create


Описание



Китайский проект с ~2100 звёздочек на github, но при этом есть минимальная документация на английском языке. Формы можно конфигурировать через JSON, но для этого используется собственный формат, отличный от оригинальной JSON schema. Присутствует тема для Element UI.


Минусы


  • Используется отличная от оригинальной JSON schema структура описания;
  • Плохая локализация на английский, как в документации, так и создаваемом представлении, во многих местах отсутствует перевод с китайского языка;

Плюсы


  • Присутствует возможность расширения и сторонние компоненты, такие как: Text, JSON, Code и Markdown редакторы;
  • Поддержка Element UI.

Дополнительные проекты


vue-vuelidate-jsonschema



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


json2vue



Проект создан для конфигурирования форм с помощью JSON, но ценность этого сомнительна, т.к. там нет валидаторов, локализации и т.д.


vue-form-builder



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


Выводы


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


  • Взять vue-form-json-schema и сделать для него свои компоненты, базирующиеся на Element UI, а также сделать генератор uiSchema объектов, который будет иметь более простой интерфейс, где не нужно будет создавать развесистые описания;
  • Взять vue-json-schema и добавить туда локализацию объектов, но это потребует больших изменений под капотом, а также необходимо расширять интерфейс описания объектов, чтобы поддержать все возможности Element UI;
  • Взять ncform и написать конвертер для оригинальной JSON schema в кастомную под этот проект, также необходимо расширить плагины интеграции с Element UI, чтобы поддержать больше возможностей из этого фреймворка;
  • Взять vue-form-generator и написать конвертер для оригинальной JSON schema в кастомную под этот проект, реализовать способ внедрения переводов или поддержки i18n, переработать стилизацию ошибок под Element UI.

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


  • Создавать формы только на основе оригинальной JSON schema;
  • Поддерживать локализацию объектов внутри формы и ошибок от валидаторов;
  • Поддерживать гибкий механизм проверки входных;
  • Поддерживать визуальные схемы Element UI.

В рамках результата исследования, выбор пал на ncform как на самое готовое решение. JSON schema требует минимальной кастомизации и её генерация будет практически нативной, есть поддержка Element UI, локализация ошибок возможна как через внедрение собственного описания при загрузке библиотеки, так и через минимальную правку исходного кода, текст ошибок валидации задаётся на уровне схемы, но конкретно эти участки должны подвергаться предобработке до генерации формы.


Если ваш выбор, как и выбор автора, остановится на ncform, то можно использовать данный fork: github и npm. В рамках него проделана работа по переводу ошибок от стандартных валидаторов на русский и английский языки из коробки, расширена функциональность визуальных компонентов Element UI, исправлена работа некоторых валидаторов, например, для списковых объектов с типом array.


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

Подробнее..
Категории: Vuejs , Vue , Vue.js , Json-schema , Form generator , Element

Перевод Создание трекера пульсоксиметрии с использованием 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.Исторические данные могут отображаться в виде диаграммы тенденций, а также могут быть доступны другим пользователям.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    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