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

Фреймворк

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

08.07.2020 16:06:56 | Автор: admin
Вот перевод второго урока учебного курса по Vue.js. Здесь речь пойдёт о привязке атрибутов, о подключении данных, хранящихся в экземпляре Vue, к атрибутам HTML-элементов.



Первый урок

Цель урока


Здесь мы разберёмся с тем, как, используя привязку атрибутов, вывести изображение, и задать текст атрибута alt. Соответствующие данные мы возьмём из экземпляра Vue.

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


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

<div id="app"><div class="product"><div class="product-image"><img src="" /></div><div class="product-info"><h1>{{ product }}</h1></div></div></div>

Обратите внимание на тег <div> с классом product-image. Именно в нём содержится элемент <img>, к которому мы хотим динамически привязать данные, необходимые для вывода изображения.

Элемент <div> с классом product-info используется для вывода названия товара.

Вот JavaScript-код, содержащийся в файле main.js:

var app = new Vue({el: '#app',data: {product: "Socks",image: "./assets/vmSocks-green.jpg"}})

Обратите внимание на то, что в объекте data теперь имеется новое свойство image, содержащее путь к изображению.

Здесь можно найти CSS-код, используемый в этом уроке.

Для подключения стиля к index.html нужно добавить в тег <head> следующее:

<link rel="stylesheet" type="text/css" href="style.css"

Тут мы исходит из предположения о том, что файл со стилями имеет имя style.css и хранится в той же папке, что и index.html.

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

Задача


Нам нужно, чтобы на странице вывелось бы изображение. При этом мы хотим динамически управлять этим изображением. То есть, нам нужна возможность, позволяющая менять путь к изображению, хранящийся в экземпляре Vue, и тут же видеть результаты этих изменений на странице. Так как именно атрибут src элемента <img> отвечает за то, какое изображение выведет элемент, нам нужно привязать некие данные к этому атрибуту. Это и позволит нам динамически, основываясь на данных, хранящихся в экземпляре Vue, менять изображение.

Важный термин: привязка данных


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

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

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


Для того чтобы привязать значение свойства image из объекта с данными к свойству src тега <img>, мы воспользуемся директивой Vue v-bind. Перепишем код тега <img> из файла index.html:

<img v-bind:src="image" />

Когда Vue, обрабатывая страницу, видит такую конструкцию, фреймворк заменяет её на следующий HTML-код:

<img src="./assets/vmSocks-green.jpg" />

Если всё сделано правильно, то на странице будет выведено изображение.


Изображение зелёных носков выведено на странице

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

Предположим, нам надо заменить изображение зелёных носков на изображение синих. Для этого, учитывая то, что путь к файлу с новым изображением выглядит как ./assets/vmSocks-blue.jpg (файл изображения можно найти здесь), достаточно привести код описания свойства image в объекте data к такому виду:

image: "./assets/vmSocks-blue.jpg"

Это приведёт к тому, что на странице появится изображение синих носков.


Изображение синих носков выведено на странице

Дополнительные варианты использования v-bind


Директиву v-bind можно использовать не только с атрибутом src. Она может помочь нам и в динамической настройке атрибута изображения alt.

Добавим в объект с опциями data новое свойство altText:

altText: "A pair of socks"

Привяжем соответствующие данные к атрибуту alt, приведя код элемента <img> к такому виду:

<img v-bind:src="image" v-bind:alt="altText" />

Здесь, как и в случае с атрибутом src, используется конструкция, состоящая из v-bind, двоеточия и имени атрибута (alt).

Теперь, если в данных экземпляра Vue изменятся свойства image или altText, в соответствующие атрибуты элемента <img> попадут обновлённые данные. При этом связь атрибутов элемента и данных, хранящихся в экземпляре Vue, не нарушится.

Этот приём постоянно используется при разработке Vue-приложений. Из-за этого существует сокращённый вариант записи конструкции v-bind:имяатрибута. Он выглядит как :имяатрибута. Если использовать этот приём при написании кода тега <img>, то получится следующее:

<img :src="image" />

Это просто и удобно. Данный приём улучшает чистоту кода.

Практикум


Добавьте на страницу ссылку (элемент <a>) с текстом More products like this. В объекте data создайте свойство link, содержащее ссылку https://www.amazon.com/s/ref=nb_sb_noss?url=search-alias%3Daps&field-keywords=socks. Свяжите, используя директиву v-bind, свойство link с атрибутом href элемента <a>.

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

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

Итоги


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

  • Данные, хранящиеся в экземпляре Vue, можно привязывать к HTML-атрибутам.
  • Для привязки данных к атрибутам используется директива v-bind. Сокращённая запись этой директивы выглядит как двоеточие (:).
  • Имя атрибута, которое идёт за двоеточием, указывает на атрибут, к которому осуществляется привязка данных.
  • В качестве значения атрибута, указываемого в кавычках, используется имя ключа, по которому можно найти данные, подключаемые к атрибуту.

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

Подробнее..

Перевод Vue.js для начинающих, урок 4 рендеринг списков

15.07.2020 16:21:33 | Автор: admin
Сегодня, в четвёртом уроке учебного курса по Vue, мы поговорим о том, как выводить на страницу списки элементов.



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

Цель урока


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

  • 80% cotton
  • 20% polyester
  • Gender-neutral

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


Начнём работу с такого HTML-кода (файл index.html):

<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></div></div>

Вот как будет выглядеть объект data, используемый при создании экземпляра Vue в main.js:

data: {product: "Socks",image: "./assets/vmSocks-green.jpg",inStock: true,details: ['80% cotton', '20% polyester', 'Gender-neutral']}

Здесь появилось новое свойство массив details.

Задача


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

details: ['80% cotton', '20% polyester', 'Gender-neutral']

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


Тут нам поможет ещё одна директива Vue v-for. Она позволяет перебирать массивы и выводить содержащиеся в них данные.

Добавим в index.html следующий код:

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

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


Список на странице

Синтаксическая конструкция, используемая в кавычках вместе с директивой v-for, покажется знакомой тем, кто пользовался JavaScript-циклами for of или for in. Поговорим о том, как работает директива v-for.

Здесь мы используем существительное в единственном числе (detail) в качестве псевдонима для строковых значений, извлекаемых из массива. Затем мы пишем in и указываем имя коллекции, которую перебираем (details). В двойных фигурных скобках указывается то, какие именно данные мы хотим выводить ({{ detail }}).

Так как конструкция v-for находится внутри элемента <li>, Vue выведет новый элемент <li> для каждого элемента массива details. Если бы директива v-for использовалась внутри элемента <div>, тогда для каждого элемента массива выводился бы элемент <div>, визуализирующий значение этого элемента массива.

Директиву v-for можно представить себе в виде конвейера, на котором имеется манипулятор. Он берёт элементы коллекции, по одному за раз, и собирает список.

image
Директива v-for похожа на конвейер

Рассмотрим ещё один пример применения v-for, более сложный. Здесь мы будем выводить в элементе <div> данные, хранящиеся в массиве объектов.

Перебор массива объектов


Карточка товара, разработкой которой мы занимаемся, нуждается в возможности выводить сведения о разных вариантах одного и того же товара. Эти сведения содержатся в массиве объектов variants, который хранится в объекте с данными data. Как перебрать этот массив объектов для вывода данных?

Вот массив, о котором идёт речь:

variants: [{variantId: 2234,variantColor: 'green'},{variantId: 2235,variantColor: 'blue'}]

В объектах, которые содержатся в данном массиве, имеется название цвета и идентификатор варианта товара.

Выведем эти данные на странице:

<div v-for="variant in variants"><p>{{ variant.variantColor }}</p></div>


Список вариантов товара

Здесь нам нужно вывести на страницу лишь название цвета, соответствующее разным вариантам товара. Поэтому мы, обращаясь к элементам массива, используем точечную нотацию. Если бы мы, в фигурных скобках, написали {{ variant }}, то на страницу вывелся бы весь объект.

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

<div v-for="variant in variants" :key="variant.variantId"><p>{{ variant.variantColor }}</p></div>

Практикум


Добавьте в объект с данными массив sizes, содержащий сведения о размерах носков, и, используя директиву v-for, выведите данные из этого массива на странице в виде списка.

Массив sizes может выглядеть так:

sizes: ['S', 'M', 'L', 'XL', 'XXL', 'XXXL']

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

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

Итоги


Сегодня мы узнали следующее:

  • Директива v-for позволяет перебирать массивы для вывода содержащихся в них данных.
  • В конструкции v-for для доступа к элементам массива используется псевдоним. Здесь же указывается и имя самого массива. Например, это может выглядеть так: v-for=item in items.
  • При переборе массива объектов можно использовать точечную нотацию для доступа к свойствам объектов.
  • При использовании v-for рекомендуется назначать каждому выводимому элементу уникальный ключ.

Заглядываете ли вы в документацию Vue, занимаясь по этому курсу?

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

Подробнее..

Перевод 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: формы

Подробнее..

GraphQL Typescript любовь. TypeGraphQL v1.0

26.08.2020 14:06:49 | Автор: admin


ЗTypeGraphQL v1.0


19 августа вышел в релиз фреймворк TypeGraphQL, упрощающий работу с GraphQL на Typescript. За два с половиной года проект обзавёлся солидным комьюнити и поддержкой нескольких компаний и уверено набирает популярность. Спустя более 650 коммитов у него более 5000 звёзд и 400 форков на гитхабе, плод упорной работы польского разработчика Михала Литека. В версии 1.0 значительно улучшилась производительность, схемы получили изоляцию и избавились от прежней избыточности, появились две крупные фичи директивы и расширения, фреймворк был приведён к полной совместимости с GraphQL.

Для чего этот фреймворк?


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

  1. Сначала нужно создать все необходимые типы GraphQL в формате SDL (Schema Definition Language);
  2. Затем мы создаём модели данных в ORM (Object-Relational Mapping), чтобы описать объекты в базе данных;
  3. После этого надо написать преобразователи для всех запросов, мутаций и полей, что вынуждает нас...
  4. Создавать тайпскриптовые интерфейсы для всех аргументов, инпутов и даже типов объектов.
  5. Только после этого преобразователями можно пользоваться, не забывая при этом вручную проверять рутинные вещи вроде валидации, авторизации и загрузки зависимостей.


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

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

Принцип работы


Разберём работу TypeGraphQL на примере GraphQL API для базы рецептов.

Так будет выглядеть схема в SDL:

    type Recipe {        id: ID!        title: String!        description: String        creationDate: Date!        ingredients: [String!]!    }


Перепишем её в виде класса Recipe:

    class Recipe {        id: string;        title: string;        description?: string;        creationDate: Date;        ingredients: string[];    }


Снабдим класс и свойства декораторами:

    @ObjectType()    class Recipe {        @Field(type => ID)        id: string;        @Field()        title: string;        @Field({ nullable: true })        description?: string;        @Field()        creationDate: Date;        @Field(type => [String])        ingredients: string[];    }


Подробные правила описания полей и типов в соответствующем разделе документации

Затем мы опишем обычные CRUD запросы и мутации. Для этого создадим контроллер RecipeResolver c RecipeService, переданным в конструктор:

    @Resolver(Recipe)    class RecipeResolver {        constructor(private recipeService: RecipeService) {}        @Query(returns => Recipe)        async recipe(@Arg("id") id: string) {            const recipe = await this.recipeService.findById(id);            if (recipe === undefined) {                throw new RecipeNotFoundError(id);            }            return recipe;        }        @Query(returns => [Recipe])        recipes(@Args() { skip, take }: RecipesArgs) {            return this.recipeService.findAll({ skip, take });        }        @Mutation(returns => Recipe)        @Authorized()        addRecipe(            @Arg("newRecipeData") newRecipeData: NewRecipeInput,            @Ctx("user") user: User,        ): Promise<Recipe> {            return this.recipeService.addNew({ data: newRecipeData, user });        }        @Mutation(returns => Boolean)        @Authorized(Roles.Admin)        async removeRecipe(@Arg("id") id: string) {            try {                await this.recipeService.removeById(id);                return true;            } catch {                return false;            }        }    }


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

Пора добавить NewRecipeInput и RecipesArgs:

@InputType()class NewRecipeInput {@Field()@MaxLength(30)title: string;@Field({ nullable: true })@Length(30, 255)description?: string;@Field(type => [String])@ArrayMaxSize(30)ingredients: string[];}@ArgsType()class RecipesArgs {@Field(type => Int, { nullable: true })@Min(0)skip: number = 0;@Field(type => Int, { nullable: true })@Min(1) @Max(50)take: number = 25;}


Length, Min и @ArrayMaxSize это декораторы из класса-валидатора, которые автоматически выполняют проверку полей.

Последний шаг собственно сборка схемы. Этим занимается функция buildSchema:

    const schema = await buildSchema({        resolvers: [RecipeResolver]    });


И всё! Теперь у нас есть полностью рабочая схема GraphQL. В скомпилированном виде она выглядит так:

type Recipe {id: ID!title: String!description: StringcreationDate: Date!ingredients: [String!]!}input NewRecipeInput {title: String!description: Stringingredients: [String!]!}type Query {recipe(id: ID!): Reciperecipes(skip: Int, take: Int): [Recipe!]!}type Mutation {addRecipe(newRecipeData: NewRecipeInput!): Recipe!removeRecipe(id: ID!): Boolean!}


Это пример базовой функциональности, на самом деле TypeGraphQL умеет использовать ещё кучу инструментов из арсенала TS. Ссылки на документацию вы уже видели :)

Что нового в 1.0


Вкратце пройдёмся по основным изменениям релиза:

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


TypeGraphQL это по сути дополнительный слой абстракции над библиотекой graphql-js, и он всегда будет работать медленнее её. Но теперь, по сравнению с версией 0.17, на выборке из 25000 вложенных объектов фреймфорк добавляет в 30 раз меньше оверхеда с 500% до 17% с возможностью ускорения до 13%. Отдельные нетривиальные способы оптимизации описаны в документации.

Изоляция схем


В старых версиях схема строилась из всех метаданных, получаемых из декораторов. Каждый последующий вызов buildSchema возвращал одну и ту же схему, построенную из всех доступных в хранилище метаданных. Теперь же схемы изолированы и buildSchema выдаёт только те запросы, которые напрямую связаны с заданными параметрами. То есть изменяя лишь параметр resolvers мы поулчаем разные операции над схемами GraphQL.

Директивы и расширения


Это два способа добавить метаданные в элементы схемы: директивы GraphQL являются часть SDL и могут быть объявлены прямо в схеме. Также они могут изменять её и выполнять специфические операции, например, сгенерировать тип соединения для пагинации. Применяются они с помощью декораторов @Directive и @Extensions и различаются подходом к построению схемы. Документация Directives, Документация Extensions.

Преобразователи и аргументы для полей интерфейсов


Последний рубеж полной совместимости с GraphQL лежал здесь. Теперь можно определять преобразователи для полей интерфейса, используя синтаксис @ObjectType:

    @InterfaceType()    abstract class IPerson {        @Field()        avatar(@Arg("size") size: number): string {            return `http://i.pravatar.cc/${size}`;        }    }


Немногочисленные исключения описаны здесь.

Преобразование вложенных инпутов и массивов


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

Заключение


В течение всего времени разработки, проект оставался открытым к идеям и критике, опенсорсным и зажигательным. 99% кода написал сам Михал Литек, но и сообщество внесло огромный вклад в развитие TypeGraphQL. Теперь, с нарастающей популярностью и финансовой поддержкой, он может стать настоящим стандартом в своей области.

Сайт
Гитхаб
Доки
Твиттер Михала

Подробнее..

Фрактальная шизофрения. Whats up?

01.04.2021 04:04:20 | Автор: admin


По некоторым источникам еще в IV до нашей эры Аристотель задался одним простым вопросом Что было раньше? Курица или яйцо? Сам он в итоге пришел к выводу, что и то, и другое появилось одновременно вот это поворот! Не правда ли?


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


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


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


What's up guys?


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


npm i whatsup

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


Построен он на генераторах, из коробки даёт функционал аналогичный react + mobx, не уступает по производительности, при этом весит менее 5kb gzip.


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


Cause & Conse


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


Начнём со следствия. По сути это одноимённая причина, которой из вне можно задавать значение возвращаемого следствия (маслянистое определение получилось, но чисто технически всё так и есть).


const name = conse('John')// И мы ему такие - What`s up name?whatsUp(name, (v) => console.log(v))// а он нам://> "John"name.set('Barry')//> "Barry"

Пример на CodeSandbox


Ничего особенного, правда? conse создает поток с начальным значением, whatsUp "вешает" наблюдателя. С помощью .set(...) меняем значение наблюдатель реагирует в консоли появляется новая запись.


На самом деле Conse это частный случай потока Cause. Последний создается из генератора, внутри которого выражение yield* это "подключение" стороннего потока к текущему, иными словами обстановку внутри генератора можно рассмотреть так, как будто бы мы находимся внутри изолированной комнаты, в которую есть несколько входов yield* и всего один выход return (конечно же yield ещё, но об этом позже)


const name = conse('John')const user = cause(function* () {    return {        name: yield* name,        //    ^^^^^^ подключаем поток name        //           пускаем его данные в комнату    }})// И мы ему такие - What`s up user? :)whatsUp(user, (v) => console.log(v))// а он нам://> {name: "John"}name.set('Barry')//> {name: "Barry"}

Пример на CodeSandbox


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


И в чем тут соль генераторов?


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


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


const name = conse('John')let revision = 0const user = cause(function* () {    return {        name: yield* name,        revision: revision++,    }})whatsUp(user, (v) => console.log(v))//> {name: "John", revision: 0}name.set('Barry')//> {name: "Barry", revision: 1}

Пример на CodeSandbox


Что-то подсказывает мне, что так не красиво revision выглядит оторваной от контекста и незащищенной от воздействия из вне. Этому есть решение мы можем поместить определение этой переменной в тело генератора, а для отправки нового значения в поток (выхода из комнаты) использовать yield вместо return, что позволит нам не завершать выполнение генератора, а приостанавливать и возобновлять с места последней остановки при следующем обновлении.


const name = conse('John')const user = cause(function* () {    let revision = 0    while (true) {        yield {            name: yield* name,            revision: revision++,        }    }})whatsUp(user, (v) => console.log(v))//> {name: "John", revision: 0}name.set('Barry')//> {name: "Barry", revision: 1}

Пример на CodeSandbox


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


Расширенный пример


Функции cause и conse это шорты для создания потоков. Существуют одноименные базовые классы, доступные для расширения.


import { Cause, Conse, whatsUp } from 'whatsup'type UserData = { name: string }class Name extends Conse<string> {}class User extends Cause<UserData> {    readonly name: Name    constructor(name: string) {        super()        this.name = new Name(name)    }    *whatsUp() {        while (true) {            yield {                name: yield* this.name,            }        }    }}const user = new User('John')whatsUp(user, (v) => console.log(v))//> {name: "John"}user.name.set('Barry')//> {name: "Barry"}

Пример на CodeSandbox


При расширении нам необходимо реализовать метод whatsUp, возвращающий генератор.


Контекст и диспозинг


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


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


Рассмотрим пример потока-таймера, который с задержкой в 1 секунду, используя setTimeout, генерирует новое значение, а при уничтожении вызывает clearTimeout для очистки таймаута.


const timer = cause(function* (ctx: Context) {    let timeoutId: number    let i = 0    try {        while (true) {            timeoutId = setTimeout(() => ctx.update(), 1000)            // устанавливаем таймер перезапуска с задержкой 1 сек            yield i++            // отправляем в поток текущее значение счетчика            // заодно инкрементим его        }    } finally {        clearTimeout(timeoutId)        // удаляем таймаут        console.log('Timer disposed')    }})const dispose = whatsUp(timer, (v) => console.log(v))//> 0//> 1//> 2dispose()//> 'Timer disposed'

Пример на CodeSandbox


Мутаторы всё из ничего


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


const increment = mutator((i = -1) => i + 1)const timer = cause(function* (ctx: Context) {    // ...    while (true) {        // ...        // отправляем мутатор в поток        yield increment    }    // ...})

Пример на CodeSandbox


Мутатор устроен очень просто это метод, который принимает предыдущее значение и возвращает новое. Чтобы он заработал нужно всего лишь вернуть его в качестве результата вычислений, вся остальная магия произойдет под капотом. Поскольку при первом запуске предыдущего значения не существует, мутатор получит undefined, параметр i по умолчанию примет значение -1, а результатом вычислений будет 0. В следующий раз ноль мутирует в единицу и т.д. Как вы уже заметили increment позволил нам отказаться от хранения локальной переменной i в теле генератора.


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


class EqualArr<T> extends Mutator<T[]> {    constructor(readonly next: T[]) {}    mutate(prev?: T[]) {        const { next } = this        if (            prev &&             prev.length === next.length &&             prev.every((item, i) => item === next[i])        ) {            /*            Возвращаем старый массив, если он эквивалентен новому,             планировщик сравнит значения, увидит,             что они равны и остановит бессмысленные пересчеты            */            return prev        }        return next    }}const some = cause(function* () {    while (true) {        yield new EqualArr([            /*...*/        ])    }})

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


Также как cause и conse функция mutator это шорт для краткого определения простого мутатора. Более сложные мутаторы можно описать, расширяя базовый класс Mutator, в котором необходимо реализовать метод mutate.


Смотрите вот так можно создать мутатор dom-элемента. И поверьте элемент будет создан и вставлен в body однократно, всё остальное сведётся к обновлению его свойств.


class Div extends Mutator<HTMLDivElement> {    constructor(readonly text: string) {        super()    }    mutate(node = document.createElement('div')) {        node.textContent = this.text        return node    }}const name = conse('John')const nameElement = cause(function* () {    while (true) {        yield new Div(yield* name)    }})whatsUp(nameElement, (div) => document.body.append(div))/*<body>    <div>John</div></body>*/name.set('Barry')/*<body>    <div>Barry</div></body>*/

Пример на CodeSandbox


Так это ж стейт менеджер на генераторах


Да с одной стороны WhatsUp это стейт менеджер на генераторах, в нём есть аналоги привычных observable, computed, reaction. Есть и action, позволяющий внести несколько изменений и провести обновление за один проход. Пока что ничего необычного, но то что вы увидите дальше, выгодно отличает его от других систем управления состоянием.


Фракталы


Я же говорил, что сохранил идею :) Особенность фрактала заключается в том, что для каждого потребителя он создает персональный генератор и контекст. Он как по лекалу создает новую, параллельную вселенную, в которой своя жизнь, но те же правила. Контексты соединяются друг с другом в отношения parent-child получается дерево контекстов, по которому организуется спуск данных вниз к листьям и всплытие событий вверх к корню. Контекст и система событий, Карл! Пример ниже длинный, но наглядно демонстрирует и то, и другое.


import { Fractal, Conse, Event, Context } from 'whatsup'import { render } from '@whatsup/jsx'class Theme extends Conse<string> {}class ChangeThemeEvent extends Event {    constructor(readonly name: string) {        super()    }}class App extends Fractal<JSX.Element> {    readonly theme = new Theme('light');    readonly settings = new Settings()    *whatsUp(ctx: Context) {        // расшариваем поток this.theme для всех нижележащих фракталов        // т.е. "спускаем его" вниз по контексту        ctx.share(this.theme)        // создаем обработчик события ChangeThemeEvent, которое можно        // инициировать в любом нижележащем фрактале и перехватить тут        ctx.on(ChangeThemeEvent, (e) => this.theme.set(e.name))        while (true) {            yield (<div>{yield* this.settings}</div>)        }    }}class Settings extends Fractal<JSX.Element> {    *whatsUp(ctx: Context) {        // берем поток Theme, расшаренный где-то в верхних фракталах        const theme = ctx.get(Theme)        // инициируем всплытие события, используя ctx.dispath        const change = (name: string) =>             ctx.dispath(new ChangeThemeEvent(name))        while (true) {            yield (                <div>                    <h1>Current</h1>                    <span>{yield* theme}</span>                    <h1>Choose</h1>                    <button onClick={() => change('light')}>light</button>                    <button onClick={() => change('dark')}>dark</button>                </div>            )        }    }}const app = new App()render(app)

Пример на CodeSandbox


Метод ctx.share используется для того, чтобы сделать экземпляр какого-то класса доступным для фракталов стоящих ниже по контексту, а вызов ctx.get с конструктором этого экземпляра позволяет нам найти его.


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


Я настолько заморочился, что написал свой jsx-рендер и babel-плагин для трансформации jsx-кода. Уже догадываетесь что под капотом? Да мутаторы. Принцип тот же, что и в примере с мутатором dom-элемента, только тут создается и в дальнейшем мутируется определенный фрагмент html-разметки. Создания и сравнения всего виртуального dom (как в react, например) не происходит. Всё сводится к локальным пересчетам, что даёт хороший прирост в производительности. Иными словами в примере выше, при изменении темы оформления, перерасчеты и обновление dom произойдут только во фрактале Settings (потому что yield* theme поток подключен только там).


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


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


Обработка ошибок


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


import { conse, Fractal } from 'whatsup'import { render } from '@whatsup/jsx'class CounterMoreThan10Error extends Error {}class App extends Fractal<JSX.Element> {    *whatsUp() {        const clicker = new Clicker()        const reset = () => clicker.reset()        while (true) {            try {                yield (<div>{yield* clicker}</div>)            } catch (e) {                // ловим ошибку, если "наша" - обрабатываем,                // иначе отправляем дальше в поток и даем возможность                 // перехватить её где-то в вышестоящих фракталах                if (e instanceof CounterMoreThan10Error) {                    yield (                        <div>                            <div>Counter more than 10, need reset</div>                            <button onClick={reset}>Reset</button>                        </div>                    )                } else {                    throw e                }            }        }    }}class Clicker extends Fractal<JSX.Element> {    readonly count = conse(0)    reset() {        this.count.set(0)    }    increment() {        const value = this.count.get() + 1        this.count.set(value)    }    *whatsUp() {        while (true) {            const count = yield* this.count            if (count > 10) {                throw new CounterMoreThan10Error()            }            yield (                <div>                    <div>Count: {count}</div>                    <button onClick={() => this.increment()}>increment</button>                </div>            )        }    }}const app = new App()render(app)

Пример на CodeSandbox


Мне банально непонятен весь этот звездочный код


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


  • yield* подключить поток и извлечь из него данные
  • yield отправить данные в поток
  • return отправить данные в поток и пересоздать генератор
  • throw отправить ошибку в поток и пересоздать генератор

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


Естественно этот вопрос нельзя обойти стороной, поэтому я добавил whatsup в проект js-framework-benchmark. Думаю кому-то он известен, но вкратце поясню суть этого проекта заключается в сравнении производительности фреймворков при решении различных задач, как то: создание тысячи строк, их замена, частичное обновление, выбор отдельной строки, обмен двух строк местами, удаление и прочее. По итогам тестирования собирается подробная таблица результатов. Ниже приведена выдержка из этой таблицы, в которой видно положение whatsup на фоне наиболее популярных библиотек и фреймворков таких, как inferno, preact, vue, react и angular



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


Прочие тактико-технические характеристики


Размер


Менее 3 kb gzip. Да это размер самого whatsup. Рендер добавит еще пару кило, что в сумме даст не более 5-ти.


Glitch free


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


Глубина связей


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


Глубина связей данных не может превышать глубину стека вызовов. Да, для современных браузеров это не то, чтоб прямо очень страшно, поскольку счёт идёт минимум на десятки тысяч. Но, например, в хроме некоторой степени лежалости глубина стека вызовов всего лишь в районе 20К. Наивная попытка запилить на этом объемный граф может легко обрушиться в maximum call stack size exceeded.

Я поработал над этим моментом и теперь глубина стека не играет никакой роли. Для сравнения я реализовал один и тот же пример на mobx и whatsup (названия кликабельны). Суть примера заключается в следующем: создаётся "сеть", состоящая из нескольких слоёв. Каждый слой состоит из четырёх ячеек a, b, c, d. Значение каждой ячейки рассчитывается на основе значений ячеек предыдущего слоя по формуле a2 = b1, b2 = a1-c1, c2 = b1+d1, d2 = c1. После создания "сети" происходит вычисление значений ячеек последнего слоя. Затем значения ячеек первого слоя изменяются, что приводит к лавинообразному пересчету во всех ячейках "сети".


Так вот в Chrome 88.0.4324.104 (64-бит) mobx вывозит 1653 слоя, а дальше падает в Maximum call stack size exceeded. В своей практике я однажды столкнулся с этим в одном огромном приложении это был долгий и мучительный дебаг.


Whatsup осилит и 5, и 10 и даже 100 000 слоёв тут уже зависит от размера оперативной памяти компьютера ибо out of memory всё же наступит. Считаю, что такого запаса более чем достаточно. Поиграйтесь в примерах со значением layersCount.


Основу для данного теста я взял из репозитория реактивной библиотеки cellx (Riim спасибо).


О чем я ещё не рассказал


Делегирование


Простой и полезный механизм, благодаря которому один поток может поручить свою работу другому потоку. Всё, что для этого нужно это вызвать yield delegate(otherStream).


Асинхронные задачи


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


Роутинг


Вынесен в отдельный пакет @whatsup/route и пока что содержит в себе всего пару методов route и redirect. Для описания шаблона маршрута используются регулярные выражения, не знаю как вам, но в react-router третьей версии мне порой этого очень не хватало. Поддерживаются вложеные роуты, совпадения типа ([0-9]+) и их передача в виде потоков. Там действительно есть прикольные фишки, но рассказывать о них в рамках этой статьи мне кажется уже слишком.


CLI


Не так давно к разработке проекта подключился парень из Бразилии Andr Lins. Наличие интерфейса командной строки для быстрого старта whatsup-приложения целиком и полностью его заслуга.


npm i -g @whatsup/cli# thenwhatsup project

Попробовать


WhatsUp легко испытать где-то на задворках react-приложения. Для этого существует небольшой пакет @whatsup/react, который позволяет сделать это максимально легко и просто.


Примеры


Todos всем известный пример с TodoMVC


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


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


Sierpinski перфоманс тест, который команда реакта показывала презентуя файберы


Напоследок


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


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


Кроме того хочу выразить слова благодарности всем тем, кто меня поддерживал, писал в личку, на e-mail, в vk, telegram. Я не ожидал такой реакции после публикации первой статьи, это стало для меня приятной неожиданностью и дополнительным стимулом к развитию проекта. Спасибо!



С уважением, Денис Ч.


"Большая часть моих трудов это муки рождения новой научной дисциплины" Бенуа Мандельброт

Подробнее..

Botfather универсальный фреймворк для автоматизации

16.04.2021 20:09:46 | Автор: admin

Привет, Хабр!

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

Общие сведения

Называется эта программа Botfather. Скачать ее можно с официального сайта. Написана она с использованием библиотеки Qt и доступна как для Windows, так и для GNU/Linux. Для дистрибутивов GNU/Linux приложение доступно только в виде пакета flatpak. На официальном сайте имеется некоторое количество скриптов и довольно неплохая документация.

Итак, устанавливаем программу и запускаем. Нас встречает примерно такое окно:

Я уже добавил двух ботов. Самый первый в списке позволяет вести поиск заданного объекта на изображении. Второй умеет заходить на сайт botfather.io под определенным логином и паролем. Можно добавлять новых ботов из имеющихся в списке или создавать своих. Вот список готовых ботов:

Вызывается этот список по нажатию на "Add a bot". В программе имеется встроенный браузер, но своего редактора кода нет. Писать код для бота можно в любом текстовом редакторе, который вам по душе. Писать придется на языке JavaScript. Также в панели инструментов можно заметить кнопку "Android". С ее помощью можно подключить свой телефон или планшет и запускать ботов на мобильных устройствах. Теперь подробнее об уже добавленных мной ботах.

Image Detection Demo

Как уже говорилось, этот бот умеет искать указанный объект на изображении. Откроем его папку, перейдя во вкладку "Settings" и нажав на кнопку "Open bot folder". Мы увидим вот это:

Мы видим сам файл скрипта find_boxes.js, изображение box.png, которое следует искать и изображение screenshot.png, в котором нужно искать. Посмотрим на скрипт:

// Read the Image and Vision APIs documentation to lear more.// https://botfather.io/docs/var screenshot = new Image("screenshot.png");var box_template = new Image("box.png");var matches = Vision.findMatches(screenshot, box_template, 0.99);Helper.log(matches.length, "boxes have been found:")for (var i = 0; i < matches.length; i++) {Helper.log(matches[i]);}var output = Vision.markMatches(screenshot, matches, new Color("red"));output.save("output.png");Helper.log("The matches have been marked red on a newly created image.");Helper.log("That output image which has been saved as 'output.png'");Helper.log("Open the bots folder to view it.");

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

Судя по логам, изображение должно быть в папке. Откроем ее:

Да, действительно, бот отработал на отлично:

Посмотрим теперь на следующего бота.

Website Login Demo

Немного рассмотрим сначала код этого бота:

function main(){// Validate the user script configurationif (!Config.getValue("username") || !Config.getValue("password")){Helper.log("Please provide a Botfather username and password in the bots 'Config' tab.");return;}// Load the Botfather login pagevar browser = new Browser("Main Browser");browser.loadUrl("https://botfather.io/accounts/login/");browser.finishLoading();// Fill out the username and password fieldvar u = Config.getValue("username");var p = Config.getValue("password");browser.executeJavascript("document.getElementById('id_username').value = '" + u + "';");browser.executeJavascript("document.getElementById('id_password').value = '" + p + "';");Helper.sleep(4);// Submit the formbrowser.executeJavascript("document.getElementById('id_password').form.submit();");Helper.sleep(2);browser.finishLoading();// Tell the script user whether login succeeded or notif (browser.getUrl().toString().indexOf("/accounts/login") !== -1){Helper.log("Looks like login failed. Open the browser to check for yourself.");return;}Helper.log("Success! You're logged in. Open the browser to check for yourself.")}main();

Как видно, этот бот заходит на официальный сайт botfather.io под логином и паролем. Эти данные нужно заранее ввести во вкладке "Config":

После запуска бота во вкладке "Browsers" появится браузер. Оттуда его можно и запустить. Так как у меня нет аккаунта на сайте botfather.io, то мне в логах было показано это:

После регистрации на сайте, по указанным во вкладке "Config" данным, запускаю бота снова. Получаю следующее:

Встроенный браузер подтверждает успешный вход:

Теперь немного пробежимся по API этого фреймворка.

Небольшой обзор API

Android

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

Можно сделать скриншот:

Android.takeScreenshot();

Получить список всех пакетов:

Android.listPackages();

Запустить приложение:

Android.startApp(package);

Сымитировать тап:

Android.sendTap(location);

Свайп:

Android.sendSwipe(start, end, duration_in_ms);

И многое другое.

Desktop

Получить позицию курсора:

var mousePositionPoint = Desktop.getCursorPosition();

Клик левой кнопкой мыши:

var position = new Point(300, 300);// A simple left clickDesktop.pressMouse(position);// This is equivalent toDesktop.pressMouse(position, "left");// And also equivalent toDesktop.holdMouse(position, "left");Desktop.releaseMouse(position, "left");

Пример функции перетаскивания:

function dragAndDrop(from, to) {    Desktop.holdMouse(from, "left");    Helper.msleep(750);    // Drag is triggered by first moving the element a little    var dragTriggerOffset = from.pointAdded(new Point(25, 25));    Desktop.warpCursor(dragTriggerOffset);    Helper.msleep(750);    Desktop.releaseMouse(to, "left");}// The function is then called like this:dragAndDrop(new Point(60, 60), new Point(400, 60));

Ввод простых символов:

// Entering lowercase "a"Desktop.press("a");// Entering uppercase "A"Desktop.holdKey("shift");Desktop.pressKey("a");Desktop.releaseKey("shift");

И так далее. В разделе документации на сайте много примеров.

Browser

Загрузить страницу:

Browser.loadUrl(url);

Вызвать перезагрузку:

Browser.reload();

Выполнить код на странице:

Browser.executeJavascript(javascript_code);

Остановить загрузку браузера:

Browser.stopLoading();

Ожидание загрузки браузера:

Browser.finishLoading(timeout_seconds);

Пример создания браузеров и загрузки страниц:

var browser1 = new Browser("Browser Name 1");var browser2 = new Browser("Browser Name 2", new Size(1200, 600));browser1.loadUrl("https://google.com/");browser2.loadUrl("https://youtube.com/");browser1.finishLoading(); // (default) waits max 30 seconds for the website to loadbrowser2.finishLoading(10); // waits max 10 seconds for the website to load

На этом все! Надеюсь, что вам было интересно. До встречи в следующих постах!


Дата-центр ITSOFT размещение и аренда серверов и стоек в двух дата-центрах в Москве. За последние годы UPTIME 100%. Размещение GPU-ферм и ASIC-майнеров, аренда GPU-серверов, лицензии связи, SSL-сертификаты, администрирование серверов и поддержка сайтов.

Подробнее..

Зачем нам p3express в наших IT-проектах

20.05.2021 10:09:10 | Автор: admin

Зачем нам p3express в наших IT-проектах

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

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

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

Но есть нюанс все они требуют очень высокого уровня владения стандартом со стороны клиента, чтобы мы сработались при использовании разных с ним подходов. А это будет сложно, если клиент исповедует PRINCE2, а мы, например, топим за ISO 21500.

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

Всё жестко зафиксировано с самого начала и ничего менять нельзя. То есть мы говорим: У нас есть сроки, деньги и то, что должно в итоге получиться. Шаг влево/вправо расстрел. А проект мы считаем успешным, только если достигли вот такой цели. Это классно и замечательно, но часто даже сам клиент не готов к тому, чтобы все его требования были жестко зафиксированы в самом начале и чтобы потом их нельзя было менять. Всё равно в процессе зайдет речь о каких-то изменениях и корректировках. А если при этом вы не договорились, что это повлечет за собой изменение бюджетов, сроков или объема работ будет больно.

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

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

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

Используя этот подход, мы постепенно пришли и к тому, что для взаимодействия на одном языке с клиентами нужна общая проектная методология. Обязательно с низким порогом входа чтобы даже никогда не управляя проектами или используя другую методологию, вы могли бы быстро прочитать описание и сказать: Да, классно, я всё понял, поехали! Но под эти требования ни Agile, ни другие взрослые методологии не подходили. Зато подошел фреймворк p3express.

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

У p3express есть бешеное преимущество: он простой. Это 37 шагов, разрисованных на картинке и быстро читаемых, при этом есть примеры и шаблоны. Прочитать их за 10 минут вполне реально. Хотя, конечно, без опыта не получится начать управлять проектом прямо сразу, здесь и сейчас. Но получится понять, допустим, меня, когда я буду описывать, чем мы завтра будем заниматься, и вообще куда и как мы двигаемся. То есть если мы договорились, что работаем по p3express, то всем в любой момент времени будет понятно, как мы будем жить и куда пойдем.

Второе его преимущество p3express сбалансирован по цене/срокам/скоупу.

И еще один кайф (впрочем, как у многих других стандартов) это фреймворк. Он не описывает жестко всё, что мы должны делать (то есть не ограничивает и не сковывает), а говорит: примерно такие вопросы и такие задачи нужно решить примерно таким способом.

Дальше я поэтапно рассмотрю шаги p3express и прокомментирую, как мы с этим живем.

Подготовка проекта

Начало в p3express вполне классическое:

  • Шаг 01. Определить спонсора;

  • Шаг 02. Подготовить резюме проекта;

  • Шаг 03. Определить руководителя проекта;

  • Шаг 04. Развернуть рабочее пространство;

  • Шаг 05. Определить команду;

  • Шаг 06. Планирование проекта;

  • Шаг 07. Определить внешних исполнителей (если в проекте они есть) ;

  • Шаг 08. Провести аудит подготовки;

  • Шаг 09. Да/нет;

  • Шаг 10. Провести стартовую встречу;

  • Шаг 11. Фокусированная коммуникация.

У нас же получилось всё сильно сложнее, чем 11 шагов.

Шаг 01. Определить спонсора продукта

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

По моему мнению, владельцем является тот, кто за него платит и кто получает от него основную выгоду. На практике же это часто разные люди. Почему владелец продукта нам, как менеджерам, важен с практической точки зрения? Потому что это тот этап, когда мы спрашиваем: Кто будет определять, мы сделали всё хорошо и правильно, или нет? Кто будет принимать проект? И часто бывает, что деньги платят одни, а решение о том, заплатить эти деньги или нет, принимают другие люди. Это абсолютно нормальная история (делегирование и пр.), но нам нужно понимать, кто выставляет основные требования.

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

Шаг 02. Подготовить резюме проекта

Для нас это самое сложное перед запуском проекта. Взрослые методологии, тот же самый ISO 21500, предполагают, что на вход в проект придет бизнес-план или ТЭО, которые полностью описывают, что мы делаем и зачем, как на этом планируем зарабатывать и т.д., чтобы можно было прочитать и сказать: А, понятно!, и по нему уже запускать проект. Но у нас так не получается.

Поэтому перед стартом проекта мы обычно делаем очень много консалтинга. Эти работы мы даже выносим в отдельный контракт с фиксированной стоимостью. Обычно это относительно небольшое время от 2 до 6 недель при том, что проекты бывают от 2 до 12-24 месяцев.

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

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

Из Lean Canvas вырастает MVP: мы видим, каким должен быть минимальный продукт, чтобы проверить, а правильно ли мы вообще напридумывали скетчи и прототипы, концепт дизайна, техническое задание? Это как раз тот пакет документов, который в больших методологиях называется бизнес-план, ТЭО или еще как-то.

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

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

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

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

Параметры проекта

В итоге для любого вида бизнеса мы создаем описание проекта по таким параметрам:

Это есть во всех стандартах управления проектами, которые я знаю. Но мы используем очень простую методику Scope-Pak. Придумали ее не мы по ссылке в QR коде можно посмотреть первоисточник:

Scope-Pak это 8 шагов по 5-7 вопросов каждый. На них лучше всего отвечать всем вместе то есть собираем нужных людей, задаем им вопросы и фиксируем ответы. На этом этапе желательно, чтобы команда заказчика еще не была сформирована, так как именно в процессе встречи с теми людьми, которые могут вам помочь, вы поймете, кого из них брать в команду.

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

Рассмотрим кратко эти 8 шагов:

Шаг 1. Заинтересованные стороны:

  • Чьи деньги участвуют в реализации проекта?

  • Этот человек является спонсором проекта?

  • Кто еще будет участвовать в проекте?

Шаг 2. Компоненты:

  • Что предстоит сделать?

  • Нужны какие-то вспомогательные работы?

  • Нужны подготовительные работы?

  • Завершающие работы?

  • Если мы сделаем все это, мы завершим весь проект?

Когда мы читали оригинал, мы очень смеялись над советом: ОГРАНИЧЬТЕ СПИСОК 30 ПУНКТАМИ то есть не расписывайте подробнее, чем на 30 пунктов, что надо сделать. А на практике мы столкнулись с тем, что только первые 6 рождаются просто в бешеных муках, а потом сложно остановиться.

Шаг 3. Цели и результаты:

  • Зачем мы все это делаем и как правильно это назвать?

  • Что мы получим на выходе и как это измерить?

  • Когда мы хотим это получить?

  • То, что мы запланировали сделать даст нам эти результаты?

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

Шаг 4. Альтернативы

  • Есть ли более эффективный способ достичь своих целей?

  • То, что мы запланировали сделать единственный способ получить нужный нам результат?

  • Как еще можно получить тот же результат?

  • Что можно сделать лучше?

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

Шаг 5. Ресурсы и сложности

  • Какова стратегия финансирования проекта?

  • Насколько он важен в сравнении с другими проектами?

  • Какие ресурсы вам потребуются?

  • С какими препятствиями вы столкнетесь?

  • Кто спонсирует этот проект?

  • Какие разрешения вы должны получить?

  • Кто будет выполнять задания проекта?

  • Каковы временные рамки?

  • Каковы критические даты?

  • Что может быть еще включено в проект?

  • Что можно исключить из проекта?

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

Шаг 6. План наступления

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

Шаг 7. Риски:

  • Какие предположения вы сделали?

  • Какие предположения вы должны сделать?

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

  • Как можно сократить риски или найти обходные пути?

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

Шаг 8. Ключевые индикаторы достижения цели:

  • Кто 3-4 самые главные заинтересованные участники проекта?

  • Какой результат их скорее всего удовлетворит?

  • Как измерить каждый из них по завершении проекта?

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

Например, заказчик хочет оптимизировать логистику. Мы выясняем, что сейчас эффективность использования его автотранспорта 20%, а клиент хочет достигнуть 70%. Мы считаем на коленке и понимаем, что весь его процесс так устроен, что больше 30% просто не получится. Потому что хоть клиент и ожидает, что машина будет 70% в пути, ей на самом деле нужно больше времени для приезда, разгрузки, погрузки, отъезда и прочих факторов. И если мы соглашаемся на 70%, не оговаривая всё это сразу, то под конец приходим к тому, что проделали отличную работу, всё здорово, всё классно, все довольны, а клиент говорит: Ребята, ну не 70 же! Как минимум, это неприятно, а может закончиться и контрактными проблемами.

Шаг 9. Да / Нет

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

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

Поэтому я согласен с тем, что этап Да/Нет должен происходить как раз на этом шаге. Этим мне тоже очень понравился p3express, потому что это мало где применяется. По некоторым методологиям, люди посидели, подумали и ТЭО составили, а потом принесли на старт проект и сразу начали проектировать. А в жизни как раз получается наоборот. Когда назначен менеджер и решили да, наверное, будем делать проект, очень многое еще непонятно. И вот именно на этом этапе есть смысл сесть и подумать а вы уверены, что в это ввязываетесь? Да? Тогда поскакали, всё классно, и все друг друга хорошо понимают.

Планирование этапа

Тут у P3express всё по канонам:

  • Шаг 12. Обновить планы;

  • Шаг 13. Определить внешних исполнителей (если в проекте они есть);

  • Шаг 14. Да/нет;

  • Шаг 15. Провести стартовую встречу цикла;

  • Шаг 16. Фокусированная коммуникация.

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

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

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

Дополнительно на этапе планирования мы делаем Lean-приоритезацию она поможет в случае наступления рисков остаться в ценностных рамках. Лучше всего это делать вместе с будущим владельцем проекта и/или с лицами, принимающими решение. Схема очень простая: по двум шкалам (ценность/сложность) оцениваем по 10-балльной шкале все компоненты (фичи и прочее, что решили делать):

  • какую ценность нам это принесет?

  • что будет стоить по трудозатратам, по ресурсам, по времени, etc.?

Получаем табличку. Иногда бывает, что ценно всё то есть все наши компоненты получают десятки. Это нехорошо, и мы тогда пытаемся их оценить по 100-балльной шкале и тут уже все компоненты обычно делятся на 4 категории (очень редко бывает, что какой-то из квадратиков остается пустым).

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

Реализация этапа

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

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

  • Шаг 17. Оценить прогресс;

  • Шаг 18. Работать с отклонениями;

  • Шаг 19. Еженедельная встреча;

  • Шаг 20. Провести еженедельный аудит;

  • Шаг 21. Фокусированная коммуникация.

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

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

Два показателя особенно классные: плановый объем (PV) и освоенный объем (EV). Они помогают нам наглядно увидеть: отстаем ли мы, идем с опережением или что-то другое происходит.

Как это работает? Ресурсы редко тратятся линейно, поэтому обычно мы не можем утверждать, что за половину сроков мы потратим половину денег. На самом деле это почти всегда не так. Например, есть задачи, где в начале идет что-то очень емкое (необязательно это айтишная история) какая-то концепция, дизайн-исследование что угодно. Мы это делаем в первую неделю, а потом три недели тестируем и согласовываем то есть в это время ценности уже не добавляется, а время это занимает. Или наоборот сначала долго, но дешево копим данные, а потом быстро и дорого их анализируем. В этом случае первые 3 недели мы ничего не тратим, никакой ценности не приносим, а в конце тратим много ресурсов (денег), но получаем много ценности.

Из-за этой неровности невозможно сопоставить сроки и объем. Вроде бы прошла половина срока, и мы должны получить половину планируемого результата но мы получили 63% или 48%. Возникает вопрос: Это хорошо или плохо?. А по методике соответствия планового/освоенного мы заранее расписываем: мы будем тратить ресурсы именно так, и именно так получать ценность. А потом легко сравнить, получились ли наши запланированные 63% и 48%.

Ежедневные действия

На этом этапе p3express в основном предлагает работать с рисками:

  • Шаг 22. Зафиксировать RICs;

  • Шаг 23. Реагировать на RICs;

  • Шаг 24. Принять готовые продукты.

Мы их дополнили частью из SCRUMа, введя ежедневные митинги для обсуждения:

  • Что сделал вчера;

  • Что планируешь сегодня;

  • Что может помешать.

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

Закрытие этапа

Вот так это в p3express:

  • Шаг 25. Оценить удовлетворённость заказчика и команды;

  • Шаг 26. Запланировать улучшения;

  • Шаг 27. Фокусированная коммуникация.

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

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

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

Закрытие проекта

По p3express закрытие включает такие шаги:

  • Шаг 28. Получить одобрение и передать продукт

  • Шаг 29. Передать проект

  • Шаг 30. Оценить удовлетворенность заказчика и команды

  • Шаг 31. Провести аудит закрытия

  • Шаг 32. Извлечь уроки и заархивировать проект

  • Шаг 33. Объявить и отметить окончание проекта

  • Шаг 34. Фокусированная коммуникация

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

Сессия Скажи спасибо

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

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

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

После проекта

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

  • Шаг 35. Оценить полученные выгоды

  • Шаг 36. Спланировать дополнительные действия

  • Шаг 37. Фокусированная коммуникация

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

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


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

Подробнее..

Recovery mode Еще один фреймворк

21.02.2021 20:17:01 | Автор: admin
Основная концепция работыОсновная концепция работы

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


Вначале был конфиг

Итак, конфиг. Поскольку к этому моменту фреймворк мы уже установили. А если нет, то это легко и просто делается командой:

pip3 install idewavecore==0.0.1

Это при условии наличия у вас Python 3.6+, интернета и компьютера.

Сам конфиг при этом выглядит примерно вот так:

# settings.ymlsettings:  servers: !inject config_dir/servers.yml  db_connections:    sqlite:      host: ~      username: ~      password: ~      # default mysql 3306, postgresql 5432, sqlite don't need port      port: ~      # currently allowed: mysql, postgresql, sqlite      dialect: sqlite      # supported drivers:      # mysql: mysqlconnector, pymysql, pyodbc      # postgresql: psycopg2, pg8000, pygresql      driver: ~      # to use with sqlite this should be absolute db path      # can be empty to keep db in memory (sqlite only)      db_name: ~      charset: ~

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

# servers.ymlsample_server:  connection:    host: 1.2.3.4    port: 1234    # possible values: tcp, websocket    connection_type: tcp  # optional  proxy:    host: ~    port: ~    # possible values: tcp, websocket    connection_type: tcp  options:    server_name: Sample Server    is_debug: false  middlewares: !pipe    - !fn native.test.mock_middleware    - !fn native.test.mock_middleware    - !infinite_loop        - !fn native.test.mock_middleware        - !fn native.test.mock_middleware        - !fn native.test.mock_middleware        - !router            ROUTE_1: !fn native.test.mock_middleware            ROUTE_2: !fn native.test.mock_middleware            ROUTE_3:              - !fn native.test.mock_middleware              - !fn native.test.mock_middleware              - !fn native.test.mock_middleware  # optional  db_connection: sqlite

Здесь уже побольше тэгов.

!pipe - это специальный тэг, который создает обертку над функциями в массиве. На данный момент все функции, используемые в разделе middlewares (о них - чуть ниже), должны быть обернуты в пайп. Вкратце - пайп выполняет поочередно переданные в него функции и пробрасывает в них необходимые параметры.

!infinite_loop - это специальный тэг, который автоматически создает пайп из переданных функций и затем выполняет их внутри вечного цикла. Что может быть полезно для создания непрерывного соединения (например, по websocket).

!router - это специальный тэг, который создает маршрутизируемый пайп. Фактически, создается словарь пайпов и какой из них выполнить, роутер определяет по специальному параметру (route).

И, наконец, !fn - специальный тэг, который позволяет импортировать функцию (далее - middleware) по указанному пути. Мой фреймворк предоставляет некоторое количество миддлвэров, но можно написать свои - достаточно создать в корне своего проекта одноименную папку - middlewares - и далее создать там файлы с необходимыми функциями. Далее, чтобы использовать вашу функцию, достаточно будет вызвать тэг:

!fn <имя_вашего_файла>.<имя_функции>

И строка будет преобразована в миддлвэр. Название корневой папки (middlewares) указывать не нужно - оно будет подставлено автоматически при импорте. Если же вы хотите использовать нативные миддлвэры фреймворка (есть! я использовал три заимствованных слова подряд!), достаточно указать префикс native в пути к функции, например:

!fn native.test.mock_middleware

В целом, большой акцент сделан именно на работу с конфигом.

Middle where

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

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

from idewavecore.session import Storage, ItemFlagasync def sample_middleware(**kwargs):    global_storage: Storage = kwargs.pop('global_storage')    server_storage: Storage = kwargs.pop('server_storage')    session_storage: Storage = kwargs.pop('session_storage')    session_storage.set_items([        {            'key1': {                'value': 'some_tmp_value'            }        },        {            'key2': {                'value': 'some_persistent_value',                'flags': ItemFlag.PERSISTENT            }        },        {            'key3': {                'value': 'some_persistent_constant_value',                'flags': ItemFlag.PERSISTENT | ItemFlag.FROZEN            }        },        {            'key4': {                'value': 'some_constant_value',                'flags': ItemFlag.FROZEN            }        }    ])    value_of_key3 = session_storage.get_value('key3')

Каждый миддлвэр имеет доступ к одному из трех типов хранилищ (storage). Хранилища бывают трех типов: глобальное (global storage), хранилище сервера (server storage) и хранилище сессии (session storage).

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

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

Хранилище сессий создается персонально для каждого клиентского соединения и хранит данные в пределах этого соединения.

В основном в миддлвэрах будет использоваться именно хранилище сессии.

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

Запускаем...

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

# run.pyimport asynciofrom idewavecore import Assemblerfrom idewavecore.session import Storageif __name__ == '__main__':    loop = asyncio.get_event_loop()    global_storage = Storage()    assembler = Assembler(        global_storage=global_storage,        # ваш путь к конфигу        config_path='settings.yml'    )    servers = assembler.assemble()    for server in servers:        server.start()    loop.run_until_complete(        asyncio.gather(*[server.get() for server in servers])    )    try:        loop.run_forever()    except KeyboardInterrupt:        pass    finally:        loop.close()

Запускаем в терминале - и можем лицезреть (при условии, что вы все сделали правильно) сообщения о том, что серверы запущены. Теперь к ним можно пробовать достучаться всеми возможными способами - браузер, curl, клиент mmo rpg игры...

Что теперь

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

Присоединяйтесь https://github.com/idewave/idewavecore.

Подробнее..

Открываем доступ к Platform V опенсорсному суперфреймворку Сбера

18.05.2021 14:13:42 | Автор: admin
image

У нас примерно три тысячи команд разработки, поэтому, очевидно, нам нужен был какой-то фреймворк для разработчиков. Спустя несколько поколений эволюции мы собрали вообще всё, что было разработано в банке, в единую систему. Можно, условно, постучать по API для доступа к любому нужному сервису и получить кусочек конструктора. Сейчас мы даём доступ для разработки на этой Платформе.

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

image

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

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

Что внутри


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

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

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

image

Доступы


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

Ещё одна точка входа developer.sber.ru. Там можно уже начинать писать приложения под Платформу.

Компоненты Платформы находятся или на территории заказчика, или в наших ЦОДах в России, то есть сразу обеспечивается полное соответствие отечественным стандартам в частности финансовой информации, персональных данных и так далее.

Компоненты


image

image

image

image

Начать работу


Ссылка на СмартМаркет раздел с документацией по Платформе. Откроем завтра.

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

Перевод Простое ускорение Java с помощью Quarkus и JHipster

01.06.2021 20:15:10 | Автор: admin

К старту курса о разработке на Java делимся переводом вводной статьи о Quarkus "родной" для Kubernetes Java-платформе для создания высокопроизводительных веб-, бессерверных (serverless) и нативных приложений (оптимизированных для используемых микропроцессоров). В ней используются предварительная компиляция AOT и агрессивная оптимизация, например сканирование путей к классам, перезагрузка конфигурации и предварительная конфигурация самозагрузки приложения в процессе сборки. Результатом становится впечатляющая скорость загрузки. Другими словами, приложения, созданные с Quarkus, запускаются не просто быстро, а очень быстро!


Так же как для платформ Spring и Micronaut, в Quarkus можно использовать преимущество GraalVM для преобразования JVM-приложений в нативные исполняемые файлы, что ещё больше повышает их быстродействие.

Такой прирост производительности позволяет этой Java-платформе конкурировать с бессерверными, облачными и созданными на основе Kubernetes средами, создавая приложения Supersonic Subatomic Java. В Quarkus используются стандарты Java (такие, как MicroProfile, JAX-RS), а также лучшие из существующих Java-библиотек (например, Hibernate и Vert.x). Есть даже поддержка аннотаций Spring.

Quarkus + JHipster = простое ускорение Java

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

JHipster это сопровождаемая сообществом разработчиков полнофункциональная платформа разработки, позволяющая создавать, развивать и развёртывать веб-приложения и ориентированные на микросервисы архитектуры. Стандартной серверной платформой в JHipster является Spring Boot, но постоянно появляются всё новые и новые возможности. Одним из таких вариантов стала blueprint-схема JHipster Quarkus.

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

Например, blueprint-схема Kotlin с JHipster является популярным дополнением, которое любят использовать разработчики. Использование blueprint-схемам многократно расширяет границы возможностей. Вот почему в JHipster есть даже реализации без Java (такие, как Node + NestJS и .NET Core). Эта публикация познакомит вас с основными шагами по использованию JHipster, blueprint-схемой Quarkus, а также протоколом OAuth для создания оптимизированного для работы на конкретных микропроцессорах и безопасного полнофункционального приложения.

Создание Java-приложения с Quarkus

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

Предварительные требования:

Установите JHipster и его blueprint-схему Quarkus, используя npm:

# Install JHipster globallynpm install -g generator-jhipster@6.10.5# Install the JHipster Quarkus blueprintnpm install -g generator-jhipster-quarkus@1.1.1

Команда jhipster-quarkus сокращение для jhipster --blueprints quarkus. Посмотреть все варианты её синтаксиса можно с помощью команды --help.

$ jhipster-quarkus --help

Создание приложения в среде JHipster Quarkus

mkdir okta-jhipster-quarkus-example && cd okta-jhipster-quarkus-example# oh-my-zsh users: take okta-jhipster-quarkus-example

Чтобы запустить мастер создания приложений, можно выполнить команду jhipster-quarkus:

jhipster-quarkus

В этой вводной статье основным вопросом будет выполнение аутентификации.

Среда JHipster Quarkus позволяет использовать JWT (с возможностью управления пользователями в базе данных приложений) или аутентификацию OAuth 2.0/OIDC с применением поставщиков идентификационной информации, таких как Keycloak и Okta. OIDC расшифровывается как OpenID Connect, этот протокол представляет собой тонкий слой поверх протокола аутентификации OAuth 2.0. Его основная задача обеспечить аутентификацию и идентификацию пользователя.

Ниже представлен пример ответов на вопросы мастера создания приложений.

После того как вы ответите на все эти вопросы, JHipster создаст код вашего приложения и выполнит команду npm install.

{  "generator-jhipster": {    "promptValues": {      "packageName": "com.mycompany.myapp",      "nativeLanguage": "en"    },    "jhipsterVersion": "6.10.5",    "applicationType": "monolith",    "baseName": "jhipster",    "packageName": "com.mycompany.myapp",    "packageFolder": "com/mycompany/myapp",    "serverPort": "8080",    "authenticationType": "oauth2",    "cacheProvider": "no",    "enableHibernateCache": true,    "websocket": false,    "databaseType": "sql",    "devDatabaseType": "h2Disk",    "prodDatabaseType": "mysql",    "messageBroker": false,    "buildTool": "maven",    "embeddableLaunchScript": false,    "useSass": true,    "clientPackageManager": "npm",    "clientFramework": "angularX",    "clientTheme": "none",    "clientThemeVariant": "",    "creationTimestamp": 1614834465776,    "jhiPrefix": "jhi",    "entitySuffix": "",    "dtoSuffix": "DTO",    "otherModules": [      {        "name": "generator-jhipster-quarkus",        "version": "1.1.1"      }    ],    "enableTranslation": true,    "nativeLanguage": "en",    "languages": ["en"],    "blueprints": [      {        "name": "generator-jhipster-quarkus",        "version": "1.1.1"      }    ]  }}

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

Убедитесь, что выполняемая с помощью Keycloak аутентификация OAuth 2.0/OIDC действительно работает

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

https://gitlab.webant.ru/russia_quiz/frontend/-/merge_requests

Если Keycloak запущен и работает, вы сможете выполнить вход в систему. Запустите ваше приложение с помощью Maven:

./mvnw

Вы можете видеть, что приложение запускается за 3,351 с, также выводится обширный список расширений Quarkus (включая oidc). Перейдите по адресу http://localhost:8080 в предпочитаемом вами браузере и нажмите ссылку sign in (вход).

Вы будете перенаправлены в Keycloak для входа в систему. При запросе идентификационных данных введите admin/admin.

После успешного прохождения аутентификации вы будете перенаправлены обратно в ваше приложение Quarkus.

Как работает поддержка протокола аутентификации OAuth 2.0 в JHipster Quarkus

Одной из проблем при разработке blueprint-схем JHipster, таких как Quarkus, является необходимость найти правильный баланс между повторным использованием существующих механизмов и добавлением индивидуальных реализаций.

С самого первого дня своего появления JHipster осуществлял интеграцию всех современных платформ для создания веб-приложений (Angular, React или даже Vue.js), совмещая их с фактически стандартными Java-технологиями, такими как Spring Boot и Spring Cloud.

В реализации JHipster Quarkus OAuth 2.0 за основу взято URI-перенаправление login/oauth2/code/oidc, предполагающее использование платформы аутентификации Spring Security. Настоятельно рекомендуем выполнять аутентификации на стороне сервера, поскольку это намного безопаснее (так как у браузера нет необходимости управлять какими-либо идентификационными данными и хранить какие-либо маркеры доступа).

В среде JHipster Quarkus, чтобы повторно использовать без изменений клиентские приложения и реализовать на серверной стороне механизм аутентификации по протоколу OAuth 2.0, в бэкенде должны быть открыты два HTTP-маршрута. Вот почему в JHipster Quarkus предусмотрен контроллер UserOauth2Controller и настраиваемые свойства Quarkus OIDC, позволяющие обеспечить правильную работу всего механизма.

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

Объединение JHipster Quarkus с сервисом Okta

Из этого раздела вы узнаете, как использовать сервис Okta в качестве провайдера протокола аутентификации OAuth 2.0/OIDC. В Okta предусмотрены два варианта для конфигурирования OIDC-приложения. Это можно выполнить либо в консоли разработчика, либо с помощью инфраструктуры Okta CLI.

Использование инфраструктуры Okta CLI для конфигурирования JHipster

Инфраструктура Okta CLI автоматизирует для вас все конфигурации JHipster и Okta. Для установки Okta CLI можно воспользоваться популярными менеджерами пакетов.

macOS (с помощью Homebrew):

brew install --cask oktadeveloper/tap/okta

Linux (с помощью Flatpak):

# Add Flathub repoflatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo# install the packageflatpak install com.okta.developer.CLI# add this to your appropriate dot filealias okta="flatpak run com.okta.developer.CLI"

Windows (с помощью Chocolatey):

choco install okta -version 0.8.0

Вы также можете просто передать его в оболочку bash:

curl https://raw.githubusercontent.com/okta/okta-cli/master/cli/src/main/scripts/install.sh | bash

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

$ okta registerFirst name: DanielLast name: PetismeEmail address: daniel.petisme@gmail.comCompany: OktaCreating new Okta Organization, this may take a minute:OrgUrl: https://dev-9323263.okta.comAn email has been sent to you with a verification code.Check your emailVerification code: 232819New Okta Account created!Your Okta Domain: https://dev-9323263.okta.comTo set your password open this link:https://dev-9323263.okta.com/welcome/drpt2SjbRAPR-gvVHhnm

Если у вас уже есть аккаунт разработчика Okta, выполните команду okta login. Затем из каталога приложения Quarkus выполните okta apps create jhipster. Примите предлагаемые по умолчанию предложения для перенаправления URI.

$ okta apps create jhipsterApplication name [okta-jhipster-quarkus-example]:Redirect URICommon defaults:  Spring Security - http://localhost:8080/login/oauth2/code/okta  Quarkus OIDC - http://localhost:8080/callback  JHipster - http://localhost:8080/login/oauth2/code/oidcEnter your Redirect URI(s) [http://localhost:8080/login/oauth2/code/oidc, http://localhost:8761/login/oauth2/code/oidc]:Enter your Post Logout Redirect URI(s) [http://localhost:8080/, http://localhost:8761/]:Configuring a new OIDC Application, almost done:Created OIDC application, client-id: 0oa5ozjxyNQPPbKc65d6Creating Authorization Server claim 'groups':Adding user daniel.petisme@gmail.com to groups: [ROLE_USER, ROLE_ADMIN]Creating group: ROLE_USERCreating group: ROLE_ADMIN

Конфигурация приложения Okta записывается здесь: /Users/daniel/workspace/okta-jhipster-quarkus-example/.okta.env

ПРИМЕЧАНИЕ: идентификаторы URI, перенаправленные http://localhost:8761*, предназначены для реестра JHipster, который часто используется при создании микросервисов с помощью JHipster. В инфраструктуре Okta CLI они добавляются по умолчанию. Они не требуются для приложения, создаваемого в этой вводной статье, но если их оставить, то никакого вреда точно не будет.

Инфраструктура Okta CLI создаст файл .okta.env в текущем каталоге. Если посмотреть на него, то вы увидите, что в нём содержатся несколько ключей и значений, предназначенных для протокола OIDC.

$ cat .okta.envexport QUARKUS_OIDC_AUTH_SERVER_URL="http://personeltest.ru/aways/dev-9323263.okta.com/oauth2/default"export QUARKUS_OIDC_CLIENT_ID="0oa5ozjxyNQPPbKc65d6"export QUARKUS_OIDC_CREDENTIALS_SECRET="KEJ0oNOTFEUEFHP7i1TELLING1xLm1XPRn"export QUARKUS_OIDC_AUTHENTICATION_REDIRECT_PATH="/login/oauth2/code/oidc"export JHIPSTER_OIDC_LOGOUT_URL="http://personeltest.ru/aways/dev-9323263.okta.com/oauth2/default/v1/logout"

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

source .okta.env./mvnw

Обязательно добавьте \*.env в ваш файл .gitignore, чтобы в коммиты не попадал ваш секрет клиента.

Как только приложение будет запущено, в окне в режиме инкогнито откройте http://localhost:8080 и выполните вход. Вам будет предложено ввести свои идентификационные данные Okta.

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

Инфраструктура Okta CLI упрощает конфигурирование JHipster и выполняет следующие полезные операции.

  1. Создаётся приложение OIDC с правильным перенаправлением URI.

  2. Заводятся группы ROLE_ADMIN и ROLE_USER, требуемые для JHipster.

  3. Ваш текущий пользователь добавляется в группы ROLE_ADMIN и ROLE_USER.

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

Но вдруг вы не любите работать из командной строки? Не паникуйте, тут есть кому прикрыть вашу спину! Инфраструктура Okta CLI проста в использовании, но для тех, кому это необходимо, есть возможность выполнить настройки с помощью интерфейса пользователя. Именно поэтому я так подробно рассматриваю каждый шаг по настройке приложения OIDC, работающего с JHipster Quarkus.

Использование консоли разработчика Okta для настройки JHipster

Если у вас нет аккаунта разработчика Okta, необходимо нажать ссылку sign up (вход). Тут нет ничего сверхсложного: просто укажите имя, фамилию, адрес электронной почты, выберите надёжный пароль и всё вы готовы двигаться дальше. Выполнив вход в систему, вы попадаете в консоль разработчика:

Разверните вложенное меню Applications (Приложения) на панели навигации слева, затем выберите в меню Applications > Create App Integration (Приложения > Создать интеграцию приложений), чтобы запустить мастер создания приложений.

Выберите OIDC и Web Application. Затем нажмите кнопку Next (Далее).

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

  • Name (Имя): можете указать любое имя на свой вкус, но разве JHipster Quarkus чем-то не подходит?

  • Login redirect URIs (Перенаправление URI при входе): определяет, будет ли Okta выполнять перенаправление в браузере клиента после аутентификации. Установите для него http://localhost:8080/login/oauth2/code/oidc, именно это значение настроено по умолчанию.

  • Logout redirect URIs (Перенаправление URI при выходе): http://localhost:8080 указывается, куда будет перенаправляться пользователь после выполнения выхода.

  • Group assignments (Назначения групп): определяет, какие группы могут использовать это приложение.

Используйте предлагаемые значения по умолчанию для всех остальных настроек. Завершив ввод требуемых настроек для своего приложения, нажмите Save (Сохранить). На экране отобразятся подробные сведения о вашем приложении.

Наиболее важными являются следующие значения:

  • идентификационные данные клиента (ID и секрет клиента). Они позволяют приложению Java выполнять аутентификацию в сервисах Okta для последующей обработки потоков аутентификации и авторизации пользователей;

  • домен Okta, из которого Quarkus будет выводить конечные точки адресов URL для протоколов OAuth/OIDC.

Создание групп пользователей

Теперь настало время для создания групп пользователей. По умолчанию для JHipster требуются две следующие группы:

  • ROLE_USER: для аутентифицированных пользователей;

  • ROLE_ADMIN: для аутентифицированных пользователей с административными правами для этого приложения.

В консоли разработчика выберите в меню Directory > Groups (Каталог > Группы). Нажмите Add Group (Добавить группу) и создайте группу ROLE_ADMIN

Теперь добавьте группу ROLE_USER.

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

Создание пользователей

Чтобы увидеть различие между обычными и административным пользователями, создайте пользователей в каждой из групп. Используя консоль разработчика Okta, выберите в меню Directory > People (Каталог > Люди). Нажмите Add Person (Добавить человека). Начните с создания пользователя Administrator.

Основным моментом тут является привязка пользователя Administrator к группе ROLE_ADMIN. Чтобы определить исходный пароль, который пользователь должен будет изменить, исключительно в демонстрационных целях для данной вводной статьи использована стратегию выбора пароля Set by Admin (Задаётся администратором). В реальном проекте я рекомендую использовать стратегию Set by User (Задаётся пользователем) с активацией по электронной почте. Теперь добавим обычного пользователя.

Убедитесь, что пользователь User входит в группу ROLE_USER. Очень важно использовать действующий адрес электронной почты, так как он может понадобиться при восстановлении пароля. Выберите в меню Applications > JHipster Quarkus (Приложения > JHipster Quarkus) и нажмите Assignments (Назначения). Назначьте группам пользователей, которых только что создали.

Добавление заявки на группы к маркеру ID

Последнее, о чём необходимо вам позаботиться, это настроить заявку на группы, включающую группы пользователей в маркер ID. Выберите в меню Security > API (Безопасность > API), а затем нажмите default (по умолчанию). Щёлкните Claims > Add Claim (Заявки > Добавить заявку). Введите следующие значения:

  • Name (Имя): groups (группы);

  • Include in token type (Включить тип маркера): ID Token (Маркер ID);

  • Value type (Тип значения): groups (группы);

  • Filter (Фильтр): Matches regex with a value of .* (Соответствует регулярному выражению со значением .*).

Нажмите Create (Создать). Конфигурация Okta для JHipster готова!

Настройка Quarkus OIDC для Okta

В этот момент необходимо, чтобы приложение JHipster Quarkus уже было запущено и для использования Keycloak в качестве провайдера идентификационных данных для него. Давайте изменим настройки для работы с Okta. Во-первых, необходимо выйти из веб-приложения JHipster, чтобы предотвратить конфликт файлов cookie. Выберите Account > Sign Out (Аккаунт > Выход).

ПРИМЕЧАНИЕ: можно оставить приложение запущенным. Quarkus поддерживает работу в так называемом режиме для разработки (Dev Mode), обеспечивающем горячую перезагрузку любых исходных или ресурсных файлов при каждом их обновлении. Это исключительно удобно!

Откройте для редактирования файл src/main/resources/application.properties и найдите в нём раздел со следующей конфигурацией OIDC.

# OAuth 2.0 and OIDCquarkus.oidc.enabled=truequarkus.oidc.auth-server-url=http://localhost:9080/auth/realms/jhipster/%dev.quarkus.oidc.client-id=web_app%dev.quarkus.oidc.credentials.secret=web_appquarkus.oidc.application-type=hybridquarkus.oidc.authentication.scopes=profile,address,email,address,phone,offline_accessquarkus.oidc.authentication.cookie-path=/quarkus.oidc.authentication.redirect-path=/login/oauth2/code/oidcquarkus.oidc.authentication.restore-path-after-redirect=falsejhipster.oidc.logout-url=http://localhost:9080/auth/realms/jhipster/protocol/openid-connect/logout%test.quarkus.oidc.client-id=dummy%test.quarkus.oidc.application-type=service%test.jhipster.oidc.logout-url=some-dummy-logoutUrl

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

  • quarkus.oidc.auth-server-url: корневой URL для API Okta, полученный из домена приложения OIDC;

  • quarkus.oidc.client-id: ID клиента для приложения OIDC;

  • quarkus.oidc.credentials.secret: секрет клиента для приложения OIDC;

  • jhipster.oidc.logout-url: в JHipster браузер будет запускать выход из системы. Серверная сторона должна предоставлять эту информацию (пока её невозможно получить с помощью OIDC-поиска).

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

# OAuth 2.0 and OIDCquarkus.oidc.enabled=truequarkus.oidc.auth-server-url=https://dev-9323263.okta.com/oauth2/defaultquarkus.oidc.client-id=0oaajhdr9q9jxbBM95d6quarkus.oidc.credentials.secret=NEVERSHOWSECRETSquarkus.oidc.application-type=hybridquarkus.oidc.authentication.scopes=profile,address,email,address,phonequarkus.oidc.authentication.cookie-path=/quarkus.oidc.authentication.redirect-path=/login/oauth2/code/oidcquarkus.oidc.authentication.restore-path-after-redirect=falsejhipster.oidc.logout-url=https://dev-9323263.okta.com/oauth2/default/v1/logout

Перезапустите приложение, перейдите по адресу http://localhost:8080. Нажмите sign in (вход) и вы будете перенаправлены на страницу входа Okta.

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

Переход в нативный формат с помощью Quarkus и GraalVM

Заключительным шагом в данной вводной статье будет упаковка приложения Java в виде нативного выполняемого файла (оптимизированного для используемого микропроцессора). И снова JHipster надёжно прикрывает ваши тылы, делая для вас всё необходимое. Просто выполните в Maven команду package с профилем native:

./mvnw package -Pnative -DskipTests

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

[error]: Build step io.quarkus.deployment.pkg.steps.NativeImageBuildStep#build threw an exception: java.lang.RuntimeException: Cannot find the `native-image` in the  GRAALVM_HOME, JAVA_HOME and System PATH. Install it using `gu install native-image`

Самым простым способом решения этой проблемы будет применение SDKMAN для установки Java 11 с GraalVM:

sdk install java 21.0.0.2.r11-grl

Затем выполните gu install native-image:

$ gu install native-imageDownloading: Component catalog from www.graalvm.orgProcessing Component: Native ImageDownloading: Component native-image: Native Image  from github.comInstalling new component: Native Image (org.graalvm.native-image, version 21.0.0.2)

Как только процесс завершится, перезапустите команду package в Maven:

./mvnw package -Pnative -DskipTests

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

  • ознакомиться с проектом JHipster Quarkus Blueprint;

  • исследовать сайт для разработчиков Okta;

  • изучить эти великолепные руководства по Quarkus.

Спустя примерно три минуты нативный выполняемый файл должен быть готов:

Запустите его как нативный выполняемый файл, используя команду target/*runner:

Ваше старое доброе Java-приложение запустится через 1 секунду! Помните, я рассказывал о приросте памяти? Ниже привожу команду, как посмотреть потребление памяти в мегабайтах:

$ ps -o pid,rss,command | grep --color jhipster | awk '{$2=int($2/1024)"M";}{ print;}'30951 46M ./target/jhipster-1.0.0-SNAPSHOT-runner31433 0M grep --color=auto --exclude-dir=.bzr --exclude-dir=CVS --exclude-dir=.git --exclude-dir=.hg --exclude-dir=.svn --color jhipster

Ваше приложение потребляет менее 50 МБ памяти. Перейдите по адресу http://localhost:8080 и убедитесь, что всё исправно работает. Теперь наслаждайтесь своим успехом!

Дальнейшая разработка с помощью JHipster Quarkus

Надеюсь, вы получили удовольствие от данной вводной статьи, пройдя все этапы создания нативного приложения и применяя для этого Java, Quarkus и JHipster. Не правда ли, впечатляет, как JHipster и Okta CLI делают для вас основную тяжёлую работу?! Вы можете найти пример, созданный в этой вводной статье, на GitHub. Если вы заинтересованы в дополнительном изучении blueprint-схем Quarkus, посмотрите проект generator-jhipster-quarkus, также размещённый на GitHub.

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

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

Другие профессии и курсы
Подробнее..

Нельзя просто взять и сделать продукт из внутреннего фреймворка или как прошел День открытых дверей Jmix

19.04.2021 16:09:25 | Автор: admin

В начале апреля мы в Haulmont анонсировали наше первое крупное онлайн-мероприятие для Java и React разработчиков в 2021 году День открытых дверей open source платформы Jmix.

Полтора часа мы говорили об эволюции продукта и шишках, которые набили на пути от внутреннего фреймворка до крупного продукта с многотысячной аудиторией. Обсуждали (в том числе и с вами) технологии, Open Source, профессию DevRel и много всего интересного. Публикуем полную запись мероприятия, а также тайм коды с отдельными докладами.

История Jmix: от внутреннего фреймворка до внешнего продукта

Рассказываем, что такое Jmix, из чего он состоит и для кого подходит, почему мы решили создать новый продукт, спустя более 10 лет работы CUBA Platform, и зачем нас понесло в Low code.

01:41

Продукт для разработчиков

Говорим, чем разработка фреймворка с открытым кодом отличается от продуктовой и заказной разработки, обсуждаем Open Source, за который не стыдно, код, за который не страшно, требования для публичного API/SDK, а также выбор технологий и библиотек.

31:22

DevRel: уникальному рынку уникальный маркетинг

Рассказываем, кто такие DevRel-специалисты, чем они занимаются, и как эта деятельность помогает развивать фреймворк.

48:58

Технологии, направления и задачи Jmix

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

56:13

JPA Buddy: инструмент для разработчиков

Обсуждаем, в чем проблемы фреймворков, почему мы выделили JPA Buddy в отдельный проект, и что он умеет.

1:13:38

Вакансии Haulmont

Без лишних слов: если вы Java или React-разработчик, будем рады видеть вас в команде Jmix.

01:20:39

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

Подробнее..

Meta Gameplay Framework, или бэкенд без серверных разработчиков

12.11.2020 12:19:49 | Автор: admin


Привет! Меня зовут Кирилл, я руководитель отдела серверной разработки в Pixonic. Здесь я работаю уже более 5 лет. Долгое время Pixonic была компанией одной игры War Robots. Но однажды к нам пришло осознание, что так больше продолжаться не может, и мы начали работу над созданием новых проектов.

Поначалу мы взялись за это дело по старинке, используя традиционные для нас подходы: писали клиент на Unity 3D, бэкенд разрабатывали на Java. Это было привычно, понятно, но имело ряд серьезных недостатков. Проекты разрабатывались медленнее, чем нам бы хотелось. Для выполнения любой задачи необходимо было задействовать как минимум двух разработчиков. Однако, когда в разработке участвуют два и более человека, неизбежно возникают ошибки в духе: то один не так понял другого, то второй работает быстрее, чем первый. Такие ситуации приводят к тому, что кому-то из разработчиков в дальнейшем приходится возвращаться к задаче, которую он, казалось, уже давно закончил, а ведь у него и других дел полно. Так мы начали думать над тем, как разрешить эту проблему.

Еще нас огорчало, что каждый раз так или иначе приходилось сталкиваться с инфраструктурными вопросами: разработка и поддержание API и схем баз данных, написание DTOшек, их преобразование из и в модели на клиенте и сервере. Эта рутина многим привычна, они ее даже не замечают, однако она отнимает время, которое можно было бы использовать с большей пользой. Эту проблему, казалось бы, можно решить клонированием предыдущего проекта но все оказывается сложнее, когда дело доходит до взаимодействия с геймдизайнерами. Они постоянно придумывают что-то новое, и даже если поначалу кажется, что у проектов много общего возьми да скопируй, то по факту оказывается, что это совсем не так. В итоге от клонированного проекта остается только набор библиотек и каркас la bootstrap.

После долгих размышлений наши желания оформились в следующие требования:

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

На момент начала разработки фреймворка на рынке уже было несколько предложений, частично удовлетворяющих нашим требованиям, PlayFab и GameSparks. Это отличные решения, построенные по модели LiveOps, но имеющие ряд критических для нас недостатков:

  • Код пишется на JavaScript, что не дает возможности полноценного переиспользования в Unity 3D, и в любом случае приходится преобразовывать ответы сервиса в модели клиента. Получается, что разработчик должен знать два языка программирования и выполнять дополнительную работу, которой хотелось бы избежать.
  • Наша цель разработка хита, а значит речь идет о миллионах игроков в день. Стоимость работы этих сервисов на наших объемах становится значительно больше, чем стоимость самостоятельного владения подобным решением.

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

  • C# (.net core для сервера и клиента, .net 3.5, 4.X для клиента). Мы хотим, чтобы разработчик мог разрабатывать как клиентскую, так и серверную часть задачи. Уйти от Unity 3D мы не можем, а вот написать сервер на C# вполне.
  • Orleans фреймворк для построения распределенных, отказоустойчивых и масштабируемых систем в модели акторов от Microsoft (использовался в Halo). Использование этого фреймворка обусловлено тем, что с нашими задачами рано или поздно придется масштабироваться к тому же, хочется, чтобы решение было отказоустойчивым.
  • GRPC для общения сервисов между собой, так как в системе, кроме сервиса игроков, построенного на Orleans, существуют и другие: авторизация, загрузка каталогов и прочее в том числе и сервисы, которые ранее были написаны на Java и оказались по-настоящему автономны и независимы от того проекта, в котором используются.
  • Postgres для хранения данных игроков. Для масштабирования базы данных мы используем шардирование.
  • Docker с ним удобно разворачивать окружение локально и в тестовой среде. Таким образом, геймдизайнеры и разработчики могут работать с метой так, чтобы никому не мешать. Можно у себя ее локально поднять, проверить, что все работает, как нужно, и запушить уже измененный код в репозиторий.
  • Prometheus для мониторинга.
  • Event Sourcing парадигма, которую мы используем для хранения данных игроков. Она часто используется в банках. Подход здесь такой: когда мы работаем через Event Sourcing, все, что мы делаем, представлено в виде потока событий. Как упоминалось ранее, хотелось бы постоянно иметь историю, которая сохраняется в базе данных. Этот поток событий и есть наша история, по которой мы можем отслеживать, что происходило. Если что-то пошло не так, мы можем посмотреть интересующее нас событие в прошлом и полностью восстановить состояние игрока.

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

Обычно настройками параметров игры занимаются геймдизайнеры. Они уже давно работают в таблицах Excel или Google Sheets, поскольку в них удобно производить расчеты, задавать формулы, строить графики. Поэтому мы решили, что хорошей идеей будет эти же таблицы использовать не только для расчетов, но и для хранения параметров игры. Единственное, чего нам не хватало формализации правил хранения данных, чтобы парсер знал, откуда эти данные брать. В итоге мы сделали несколько вариантов шаблонов вкладок под наши нужды:

  • вкладки конфигурирования игровых предметов;
  • вкладки настройки экспериментов (A/B тестов);
  • вкладки настройки лутбоксов;
  • вкладки настройки игровых валют;
  • вкладки для хранения простых настроек, заданных в виде ключ-значение.



Пример конфигурирования предмета

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

// Находим в каталоге предмет с идентификатором 'Reaver_1':ItemBlueprint itemBlueprint = catalog.GetItemBlueprint(ItemBlueprint.ValueOf("Reaver_1")); // Получаем проверенное значение из колонки с названием 'grade'. // Если поле отсутствует или имеет неверный тип, далее этот результат будет передан // в админку, где будет выведена информация о месте нахождения ошибки:Validated<int> grade = itemBlueprint.ShouldHasIntAttr("grade");

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

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

  1. Обладающие индивидуальностью и состоянием. Таким предметам можно задавать проектно специфичные параметры. Так, например, у брони может отслеживаться состояние ее износа.
  2. Не обладающие индивидуальностью. Обычно это потребляемые предметы. В таком случае хранится только счетчик их количества. Пример таких предметов бутылки с целебным зельем.

  • Лутбоксы. Сейчас без них немыслима ни одна free-to-play игра. Наше решение позволяет задавать различные варианты генерации контента для выдачи: от 100% гарантированного в этом случае лутбоксы можно использовать как обычные контейнеры, до полностью случайного.

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

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

// Описываем команду, представляющую из себя обычную DTO,// которая может быть сериализована в Json:public class DemoCommand : ICommand{    public string BlueprintId;    public int Value;} // Описываем обработчик команды:public class DemoHandler : HandlerBase, ICommandHandler<DemoCommand>{    public void Handle(ICommandContext context, DemoCommand command)    {        // Inventory  объявлен в HandlerBase.        // Создаем новый предмет по образцу из каталога:        var demoItem = Inventory.GrantItem(ItemBlueprintId.ValueOf(command.BlueprintId));         // Задаем предмету значение атрибута 'demo_value':        demoItem.Attributes["demo_value"] = command.Value;     }}

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

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

// Выполняем команду на сервере:var command = new DemoCommand { BlueprintId = "Reaver_1", Value = 777 };var commandResult = connection.Execute(command); // Из ответа получаем обновленный инвентарь игрока:var inventory = commandResult.Player.Inventory; // Получаем последний созданный предмет:var demoItem = inventory.FindItemsByBlueprint(ItemBlueprintId.ValueOf("Reaver_1")).Last(); // Выводим установленное значение:Console.WriteLine(demoItem.Attributes["demo_value"]);

Так при чем здесь Event Sourcing?

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

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

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



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

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


Пример лога транзакций из реального проекта

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

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

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

Валидация в PHP. Красота или лапша?

30.09.2020 02:10:03 | Автор: admin
Выбирая лучший PHP-валидатор из десятка популярных, я столкнулся с дилеммой. Что для меня важнее? Следование всем SOLID / ООП-канонам или удобство работы и наглядность кода? Что предпочтут пользователи фреймворка Comet? Если вы считаете, что вопрос далеко не прост добро пожаловать под кат в длинное путешествие по фрагментам кода :)


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

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

$form = [    'name'           => 'Elon Mask',     'name_wrong'     => 'Mask',    'login'          => 'mask',     'login_wrong'    => 'm@sk',     'email'          => 'elon@tesla.com',     'email_wrong'    => 'elon@tesla_com',     'password'       => '1q!~|w2o<z',     'password_wrong' => '123456',    'date'           => '2020-06-05 15:52:00',    'date_wrong'     => '2020:06:05 15-52-00',    'ipv4'           => '192.168.1.1',    'ipv4_wrong'     => '402.28.6.12',    'uuid'           => '70fcf623-6c4e-453b-826d-072c4862d133',    'uuid_wrong'     => 'abcd-xyz-6c4e-453b-826d-072c4862d133',    'extra'          => 'that field out of scope of validation',    'empty'          => ''];

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

Отраслевой стандарт и икона чистого ООП конечно же Symfony



use Symfony\Component\Validator\Constraints\Length;use Symfony\Component\Validator\Constraints\NotBlank;use Symfony\Component\Validator\Validation;use Symfony\Component\Validator\Constraints as Assert;use Symfony\Component\Translation\MessageSelector;$validator = Validation::createValidator();$constraint = new Assert\Collection([        'name' => new Assert\Regex('/^[A-Za-z]+\s[A-Za-z]+$/u'),       'login' => new Assert\Regex('/^[a-zA-Z0-9]-_+$/'),    'email' => new Assert\Email(),    'password' => [        new Assert\NotBlank(),        new Assert\Length(['max' => 64]),        new Assert\Type(['type' => 'string'])    ],    'agreed' => new Assert\Type(['type' => 'boolean'])]);$violations = $validator->validate($form, $constraint);$errors = [];if (0 !== count($violations)) {    foreach ($violations as $violation) {        $errors[] = $violation->getPropertyPath() . ' : ' . $violation->getMessage();    }} return $errors;

Вырвиглазный код на чистом PHP


$errors = [];if (!preg_match('/^[A-Za-z]+\s[A-Za-z]+$/u', $form['name']))    $errors['name'] = 'should consist of two words!';if (!preg_match('/^[A-Za-z]+\s[A-Za-z]+$/u', $form['name_wrong']))    $errors['name_wrong'] = 'should consist of two words!';if (!preg_match('/^[a-zA-Z0-9-_]+$/', $form['login']))    $errors['login'] = 'should contain only alphanumeric!';if (!preg_match('/^[a-zA-Z0-9]-_+$/', $form['login_wrong']))    $errors['login_wrong'] = 'should contain only alphanumeric!';if (filter_var($form['email'], FILTER_VALIDATE_EMAIL) != $form['email'])    $errors['email'] = 'provide correct email!';if (filter_var($form['email_wrong'], FILTER_VALIDATE_EMAIL) != $form['email_wrong'])    $errors['email_wrong'] = 'provide correct email!';if (!is_string($form['password']) ||    $form['password'] == '' ||    strlen($form['password']) < 8 ||    strlen($form['password']) > 64 )    $errors['password'] = 'provide correct password!';if (!is_string($form['password_wrong']) ||    $form['password_wrong'] == '' ||    strlen($form['password_wrong']) < 8 ||    strlen($form['password_wrong']) > 64 )    $errors['password_wrong'] = 'provide correct password!';if (!preg_match('/^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})$/', $form['date']))    $errors['date'] = 'provide correct date!';if (!preg_match('/^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})$/', $form['date_wrong']))    $errors['date_wrong'] = 'provide correct date!';if (filter_var($form['ipv4'], FILTER_VALIDATE_IP) != $form['ipv4'])    $errors['ipv4'] = 'provide correct ip4!';if (filter_var($form['ipv4_wrong'], FILTER_VALIDATE_IP) != $form['ipv4_wrong'])    $errors['ipv4_wrong'] = 'provide correct ip4!';if (!preg_match('/^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i', $form['uuid']))    $errors['uuid'] = 'provide correct uuid!';if (!preg_match('/^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i', $form['uuid_wrong']))    $errors['uuid_wrong'] = 'provide correct uuid!';if (!isset($form['agreed']) || !is_bool($form['agreed']) || $form['agreed'] != true)    $errors['agreed'] = 'you should agree with terms!';return $errors;

Решение на базе одной из самых популярных библитек Respect Validation


use Respect\Validation\Validator as v;use Respect\Validation\Factory;Factory::setDefaultInstance(    (new Factory())        ->withRuleNamespace('Validation')        ->withExceptionNamespace('Validation'));$messages = [];try {    v::attribute('name', v::RespectRule())        ->attribute('name_wrong', v::RespectRule())        ->attribute('login', v::alnum('-_'))        ->attribute('login_wrong', v::alnum('-_'))        ->attribute('email', v::email())        ->attribute('email_wrong', v::email())        ->attribute('password', v::notEmpty()->stringType()->length(null, 64))        ->attribute('password_wrong', v::notEmpty()->stringType()->length(null, 64))        ->attribute('date', v::date())        ->attribute('date_wrong', v::date())        ->attribute('ipv4', v::ipv4())        ->attribute('ipv4_wrong', v::ipv4())        ->attribute('uuid', v::uuid())        ->attribute('uuid_wrong', v::uuid())        ->attribute('agreed', v::trueVal())        ->assert((object) $form);} catch (\Exception $ex) {    $messages = $ex->getMessages();}return $messages;

Еще одно известное имя: Valitron


use Valitron\Validator;Validator::addRule('uuid', function($field, $value) {    return (bool) preg_match('/^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i', $value);}, 'UUID should confirm RFC style!');$rules = [    'required'  => [ 'login', 'agreed' ],    'regex'     => [ ['name', '/^[A-Za-z]+\s[A-Za-z]+$/'] ],    'lengthMin' => [ [ 'password', '8'], [ 'password_wrong', '8'] ],    'lengthMax' => [ [ 'password', '64'], [ 'password_wrong', '64'] ],    'slug'      => [ 'login', 'login_wrong' ],    'email'     => [ 'email', 'email_wrong' ],    'date'      => [ 'date', 'date_wrong' ],    'ipv4'      => [ 'ipv4', 'ipv4_wrong' ],    'uuid'      => [ 'uuid', 'uuid_wrong' ],    'accepted'  => 'agreed'];$validator = new Validator($form);$validator->rules($rules);$validator->rule('accepted', 'agreed')->message('You should set {field} value!');$validator->validate();return $validator->errors());

Прекрасный Sirius


$validator = new \Sirius\Validation\Validator;$validator    ->add('name', 'required | \Validation\SiriusRule')    ->add('login', 'required | alphanumhyphen', null, 'Only latin chars, underscores and dashes please.')    ->add('email', 'required | email', null, 'Give correct email please.')    ->add('password', 'required | maxlength(64)', null, 'Wrong password.')    ->add('agreed', 'required | equal(true)', null, 'Where is your agreement?');$validator->validate($form);$errors = [];foreach ($validator->getMessages() as $attribute => $messages) {    foreach ($messages as $message) {        $errors[] = $attribute . ' : '. $message->getTemplate();    }}return $errors;

А вот так валидируют в Laravel


use Illuminate\Validation\Factory as ValidatorFactory;use Illuminate\Translation\Translator;use Illuminate\Translation\ArrayLoader;use Symfony\Component\Translation\MessageSelector;use Illuminate\Support\Facades\Validator as FacadeValidator;$rules = array(    'name' => ['regex:/^[A-Za-z]+\s[A-Za-z]+$/u'],    'name_wrong' => ['regex:/^[A-Za-z]+\s[A-Za-z]+$/u'],    'login' => ['required', 'alpha_num'],    'login_wrong' => ['required', 'alpha_num'],    'email' => ['email'],    'email_wrong' => ['email'],    'password' => ['required', 'min:8', 'max:64'],    'password_wrong' => ['required', 'min:8', 'max:64'],    'date' => ['date'],    'date_wrong' => ['date'],    'ipv4' => ['ipv4'],    'ipv4_wrong' => ['ipv4'],    'uuid' => ['uuid'],    'uuid_wrong' => ['uuid'],    'agreed' => ['required', 'boolean']);$messages = [    'name_wrong.regex' => 'Username is required.',    'password_wrong.required' => 'Password is required.',    'password_wrong.max' => 'Password must be no more than :max characters.',    'email_wrong.email' => 'Email is required.',    'login_wrong.required' => 'Login is required.',    'login_wrong.alpha_num' => 'Login must consist of alfa numeric chars.',    'agreed.required' => 'Confirm radio box required.',);$loader = new ArrayLoader();$translator = new Translator($loader, 'en');$validatorFactory = new ValidatorFactory($translator);$validator = $validatorFactory->make($form, $rules, $messages);return $validator->messages();

Неожиданный бриллиант Rakit Validation


$validator = new \Rakit\Validation\Validator;$validator->addValidator('uuid', new \Validation\RakitRule);$validation = $validator->make($form, [    'name'           => 'regex:/^[A-Za-z]+\s[A-Za-z]+$/u',    'name_wrong'     => 'regex:/^[A-Za-z]+\s[A-Za-z]+$/u',    'email'          => 'email',    'email_wrong'    => 'email',    'password'       => 'required|min:8|max:64',    'password_wrong' => 'required|min:8|max:64',    'login'          => 'alpha_dash',    'login_wrong'    => 'alpha_dash',    'date'           => 'date:Y-m-d H:i:s',    'date_wrong'     => 'date:Y-m-d H:i:s',    'ipv4'           => 'ipv4',    'ipv4_wrong'     => 'ipv4',    'uuid'           => 'uuid',    'uuid_wrong'     => 'uuid',    'agreed'         => 'required|accepted']); $validation->setMessages([    'uuid'     => 'UUID should confirm RFC rules!',    'required' => ':attribute is required!',    // etc]);$validation->validate();return $validation->errors()->toArray();

Ну так что? Какой из примеров кода наиболее наглядный, идиоматичный, корректный и вообще правильный? Мой личный выбор в доках на Comet: github.com/gotzmann/comet

В заключение небольшой опрос для потомков.
Подробнее..

Новости Yii 2020, выпуск 8

29.12.2020 20:14:34 | Автор: admin

Всем привет! Это последний выпуск новостей в "весёлом" 2020 году. Я едва успел его приготовить потому как скорость разработки Yii 3 сильно подросла. Все пакеты в этом месяце мы тегнуть не успели, но многие почти готовы, поэтому стоит ожидать релизов в Январе.


С наступающим Новым Годом! Пусть будет менее сумасшедшим, чем 2020. Александр Макаров


// Не забывайте ставить звёздочки любимым пакетам на GitHub.


Фонд и команда


  • Евгений Зюбин присоединился к команде на фуллтайм для работы над Yii 3. Пока всё идёт замечательно! ы
  • Валерий Горбачев также присоединился к команде. Его вы можете знать по его работе над i18n и MSSQL.
  • Мы применили весь бюджет фонда, чтобы увеличить доступное для работы над Yii время команды. Это вылилось в сильное ускорение темпов разработки.

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


Yii 1


Был выпущен Yii 1.1.23.
Он добавляет поддержку PHP 8 и улучшает совместимость с PHP 7. Также добавлена поддержка PostgreSQL 12.


Это первый релиз, который сделал новый член команды, Marco van 't Wout, под руководством Carsten Brandt.


Yii 2


Вышел Yii 2.0.40. Фокус релиза, в основном, на исправлении ошибок.


Были выпущены следующие расширения:



Также:



Yii 3


Инфраструктура


  • Ко всем репозиториям. Применён новый набор правил StyleCI. Стиль кода стал ещё более консистентным.
  • Покрытие тестами, MSI были улучшены практически в каждом пакете.
  • Порезана история Git у некоторых пакетов, которые были получены саб-сплитом Yii 2 и таскали с собой всю его историю.
  • Включены GitHub discussions в yiisoft/app. Посмотрим, что из этого выйдет. Мы не хотим пока заменять форум, но может получиться полезно.

Релизы


  • Cookies 1.0.0. Пакет, вероятно, будет использован в форумном движке Flarum.
  • Strings 1.0.0. Кроме общей зачистки добавился метод StringHelper::split().
  • Test support 1.0.0. С последнего выпуска новостей появилась документация и набор для тестирования кеша.
  • Aliases 1.1.2.
  • Composer config plugin 0.5.0.
  • i18n 1.0.0.
  • Session 1.0.0. С последнего выпуска новостей сессия перестала запускаться когда можно обойтись без этого.
  • HTTP 1.1.0. Добавлен ContentDispositionHeader. Оно помогает сформировать кросс-браузерный заголовок, соответствующий RFC.

Новые пакеты



Мы не только добавляем пакеты, но и удаляем некоторые пакеты, такие как yii-api, были удалены.


Arrays



Хелпер Files и файловая система


В хелпере Files произошли следующие изменения:


  • Добавлен FileHelper::isEmptyDirectory().
  • Добавлен FileHelper::openFile().
  • Добавлен FileHelper::lastModifiedTime(), позволяющий получить время последней модификации содержимого директории.
  • Добавлены FileHelper::findDirectories() и FileHelper::findFiles().
  • FileHelper::unlink() поддерживает больше особенных ситуаций, в том числе под Windows.
  • FileHelper::filterPath() был заменён отдельным PathMatcher.

Пакет File system теперь использует стабильную версию Flysystem.


Консоль


Команда yii serve теперь по умолчанию использует в качестве роутера public/index.php.
Это сделано чтобы нормально поддерживались точки в URL.


БД и Active Record


Зачистка порта базы данных и AR из Yii 2 идёт полным ходом. До завершения ещё далеко, но движение в этом направлении
верное. Интересные изменения:



Миграции



Cycle



Обработчик ошибок


Применён новый дизайн к страницам ошибок. За него спасибо Аркадию Зимину и Фёдору Достоевскому.




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




Отладчик


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

Фронт-часть отладчика всё ещё разработке.


Профайлер


  • Общая зачистка и рефакторинг.
  • Добавленная конфигурация по-умолчанию.

Инструментарий разработки фреймворка


Были сделаны следующие улучшения инструмента yii-dev:


  • Команда test, которая по-тихому запускает тесты пакета и выводит только ошибки.
  • Команда git/pr/create, которая может делать GitHub pull request.
  • Команда github/settings, которая помогает менять настройки репозиториев.
  • Ошибки стали более полезными.
  • Команда replicate/copy-file, которая позволяет скопировать любой файл в любой пакет.
  • Инструмент теперь нормально работает с симлинками в контейнере Docker.
  • Команда exec теперь выводит всё в консоль сразу.
  • В команду release/make добавлены дополнительные проверки и опции. Именно она использовалась для последних релизов.
  • Рефакторинг.

Перевод сообщений


  • Отрефакторены драйверы для gettext и базы данных.
  • В README добавлена документация.

Пакет почти готов к релизу.


Демо-приложение


Мы обновляем демо вслед за пакетами:



Шаблон веб-приложения



Также обновлена начальная страница:




Шаблон API



Сейчас в шаблоне достаточно много примеров. Со временем они переедут в демо.


Кеш


Кеш серьёзно отрефакторен. Главные класс кеша теперь не реализует PSR, но использует PSR-16 как хендлеры.
Есть возможность выставить TTL и префикс по-умолчанию, есть защита от cache stampede, поддерживается инвалидация через
зависимости.


Почищены и отрефакторены большинство хендлеров. Очень вероятно что в начале года сделаем релиз.


Лог


Лог также серьёзно почищен и порефакторен. Для конечного пользователя это всё ещё PSR-совместимый логгер, но с точки
зрения конфигурации и возможностей стало интересней:


  • Можно настроить формат лога.
  • Добавлен StreamTarget для вывода в стрим.
  • Уменьшено потребление ресурсов.
  • Можно добавить свой контекст в лог.

Var dumper


  • Улучшена обработка особых случаев.
  • Больше тестов и рефакторинга.
  • Экспорт замыканий выделен в отдельный класс ClosureExporter.

Bootstrap


Изменения есть как для Bootstrap 4, так и для Bootstrap 5.



Bulma



Роутер



Serializer



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


HTML


  • Зачистка, удалены устаревшие части.
  • Добавлены Html::div(), Html::span() и Html::p().

Assets



Виджеты


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


<?= MyWidget::widget()->options(['class' => 'testMe'])->begin() ?>    Content<?= MyWidget::end() ?>

CSRF


CSRF серьёзно отрефакторен:


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

Первые сторонние пакеты


Несмотря на то, что Yii 3 ещё не релизнут, для него начали появляться первые пакеты. В этот раз обратим внимание на пакет
для работы с SVG:



Новая и обновлённая документация



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


Почитать



Спасибо!


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


Отдельное спасибо тем, кто помог Yii 3 кодом:


Подробнее..
Категории: Php , Yii , Фреймворк , Yii3 , Yii frameowork

Новости Yii 2021, выпуск 1

05.03.2021 02:11:27 | Автор: admin

Привет, сообщество!

Это первый выпуск новостей в 2021. Начало года вышло продуктивным. Мы начали активно релизить пакеты Yii 3, есть значительный прогресс с пока не релизнутыми пакетами. Улучшили инструментарий разработки, много всего исправили, убили лишние пакеты. И всё это параллельно с поддержкой Yii 2 и решением проблем с финансированием.

Команда и фонд

Несмотря на то, что 2020 был через край долбанутым, для Yii всё вышло неплохо. Удалось договориться о продлении поддержки Yii 1.1 и других вещах в обмен на единовременное или постоянное пополнение фонда.

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

Я был чрезвычайно воодушевлён возросшей скоростью разработки, окунулся в неё с головой и немного подзабыл о финансовой части. В январе меня ждал неприятный сюрприз. Оказывается, что забыл я про очень важный факт. А именно, что немаленькая часть средств фонда поступила не рекуррентными платежами. Соответственно, бюджета на всё не хватило. Чтобы не случилось полной катастрофы, я перестал забирать деньги фонда с OpenCollective и начал активно искать партнёров среди компаний, которые используют Yii и PHP. Результаты пока спорные, но, надеюсь, всё наладится.

Если хотите пообщаться на тему партнёрства, пишите вsam@rmcreative.ru.

Yii 2

Вышел Yii 2.0.41. Сильно помогли с релизомPawe Brzozowski, недавно присоединившийся к команде Yii 2, иRobert Korulczykс его тщательными ревью всего вливаемого в master кода. Много часов было потрачено на безопасность фреймворка. Удалось перебрать текущие сообщения о предполагаемых уязвимостях иулучшить безопасность.

Yii 3

Прежде всего, релизы:

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

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

Arrays

  • ДобавленArrayHelper::pathExists().

  • ДобавленArrayHelper::group(). Это алиасArrayHelper::index().

  • Удалены модификаторы.

Data

Auth

Config plugin

После большого количества попыток улучшитьComposer config pluginстало очевидно что он стал слишком сложным: AST, мёрж конфигов и всё что там творилось под капотом. Мы приняли решение сделать его менее зависимым от пакетов yiisoft и сделатьновый, более простой и производительный, пакет.

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

Error handler

  • Рефакторинг, ридми.

  • Поддержка Xdebug 3 для получения более подробных стектрейсов.

  • ExceptionResponder, при помощи которого можно формировать HTTP-ответ в зависимости от пойманной ошибки. Это может быть полезно для исключений вродеNotFoundException. Будут ли такие исключения из коробки пока обсуждаем.

  • Renderer-ы теперь могут отдавать HTTP-заголовки, добавлен renderer, который выводит ошибку в заголовках. Полезно если показывать ошибки текстом неудобно. Например, для некоторых API или при генерации картинок.

  • Элементы stacktrace для классов изvendorтеперь отображаются отдельной группой и по умолчанию свёрнуты. Так как ошибки обычно в самом приложении, это позволяет не отвлекаться на нерелевантную информацию.

Обработка ошибок в общем

  • В пакетyiisoft/yii-eventдобавленрежим отладки. В нём проверяются все обработчики событий сразу.

  • Улучшены ошибки DI контейнера.

Strings

  • ДобавленNumericHelper::isInteger().

  • Добавлена поддержкаboolвNumericHelper::normalize().

  • Был переработан матчинг по wildcard. Вместо довольно большого количества опций добавили**, совпадающий, в том числе, с сепараторами.

Var dumper

Html

Пакет был значительно переработан. Главных изменений два:

  1. Вывод по умолчанию экранируется.

  2. Теги реализованы как отдельные объекты, создаваемые через статические методы-фабрики из Html. Для пользователя это значит, что конфигурация теперь производится через вызов методов. То есть теперь IDE это дополняют и проверяют.

Валидатор

Много рефакторинга. Самое интересное:

Формы

Перевод сообщений

Почти готов к релизу. Можете почитатьего readme.

Mailer

Mailerиадаптер для SwiftMailerбыли вычищены и отрефакторены. Добавлена документация. Релизнем как только будут готовы зависимости.

DB и ActiveRecord

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

Оба пакета изначально были портированы из Yii 2 почти как есть. Была убрана магия, добавлены типы, покрытие тестами доведено почти до 100%. Но, несмотря на то что пакеты стали даже лучше, чем Yii 2, дизайн сохранился примерно в том же виде. Нам предстоит трудный выбор, релизить ли первую версию как есть или же сначала затеять гигантский рефакторинг.

Скорее всего выберем первое.

yii-web

User

Значительно переделали пакет:

Всё ещё не до конца довольны. Скорее всего будем переделывать ещё.

Bootstrap и Bulma

Консоль

Появиласьвозможность добавлять команды со своим именем.

Debugger

Есть прогресс как в API, так и на фронтенде.

Docker

Добавлена поддержка PHP 8.

Инструментарий разработки фреймворка

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

Шаблоны приложений

Новая и обновлённая документация

Почитать-посмотреть

Спасибо!

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

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

  • CraftCMS- Отличная OpenSource CMS на Yii2.

  • Onetwist Software- Услуги качественной разработки приложений.

  • SkillShare- Обучение новым навыкам.

  • Betteridge- Ювелирные изделия.

  • dmstr- Облачные решения на основе Docker.

  • HumHub- OpenSource решения для общения команды.

  • JetBrains- Отличные инструменты для разработки.

  • Skin.Club- Рынок скинов для CS:GO.

  • ЭФКО- фудтех, производство продуктов питания, и венчурные инвестиции. С недавнего времени ещё и ЭФКО-тех, отдельная растущая сервисная IT-компания, которая планирует заниматься не только внутренними проектами ЭФКО.

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

Отдельное спасибо тем, кто помог Yii 3 кодом:

Подробнее..
Категории: Php , Yii , Фреймворк , Yii2 , Yii3 , Opensource , Yii framework

Категории

Последние комментарии

  • Имя: Макс
    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