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

Front-end разработка

Перевод Vue 3 Composition API Ref или Reactive

11.10.2020 04:04:54 | Автор: admin


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

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



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

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

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

Реактивность Vue 2


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

<template>  <h1>{{ title }}</h1></template><script>  export default {    data() {      return {        title: "Hello, Vue!"      };    }  };</script>

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

REF и REACTIVE


При работе с Options API нам необходимо следовать некоторым правилам при декларировании реактивных свойств, тоже самое и при работе с Composition API. Вы не можете только создать свойство и ожидать реактивности. В следующем примере я задекларировал свойство title и функция setup() возвращает его, тем самым делая его доступным для шаблона.

<template>  <h1>{{ title }}</h1></template><script>  export default {    setup() {      let title = "Hello, Vue 3!";      return { title };    }  };</script>


Это сработает, но свойство title не будет реактивным. Т.е. если кто-то изменит title, эти изменения НЕ отразятся в Доме. Например, если вы измените title через 5 секунд, код ниже НЕ поменяет Дом.

<template>  <h1>{{ title }}</h1></template><script>  export default {    setup() {      let title = "Hello, Vue 3!";      setTimeout(() => {        title = "THIS IS A NEW TITLE";      }, 5000);      return { title };    }  };</script>


Мы можем импортировать ref и использовать ее, чтобы сделать свойство реактивным. Под капотом Vue 3 создаст Proxy.

<template>  <h1>{{ title }}</h1></template><script>  import { ref } from "vue";  export default {    setup() {      const title = ref("Hello, Vue 3!");      setTimeout(() => {        // может возникнуть вопрос, что за .value ...        // об этом позже        title.value = "New Title";      }, 5000);      return { title };    }  };</script>


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

REF


При создании свойств простых типов, ref() ваш первый выбор. Это не серебряная пуля, но с это стоит начать. Напомню вам семь примитивных типов в JavaScript:

  • String
  • Number
  • BigInt
  • Boolean
  • Symbol
  • Null
  • Undefined


import { ref } from "vue";export default {  setup() {    const title = ref("");    const one = ref(1);    const isValid = ref(true);    const foo = ref(null);  }};


В предидущем примере у нас title имеет тип String, значит чтобы сделать свойство реактивным выбираем ref(). Если код ниже вызывает у вас некоторые вопросы не беспокойтесь, у меня были такие же вопросы.

import { ref } from "vue";export default {  setup() {    const title = ref("Hello, Vue 3!");    setTimeout(() => {      title.value = "New Title";    }, 5000);    return { title };  }};


Почему мы используем const, если title будет меняться? Почему не использовать let? Если вы выведете title в консоль, вы могли бы ожидать увидеть Hello, Vue 3!, но вместо это отобразится объект:

{_isRef: true}value: (...)_isRef: trueget value:  value()set value:  value(newVal)__proto__: Object


Функция ref() примет аргумент и вернет реактивный и изменяемый ref объект. Ref объект имеет одно свойство value, ссылающееся на аргумент. Это означает, что если вы хотите получить или изменить значение надо будет использовать title.value, а так как это объект, который не будет меняться, я и объявил его const.

Обращение к REF


Следующий вопрос а почему в шаблоне мы не обращаемся title.value?

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


Когда Ref возвращается как свойство в контекст отрисовки (объект возвращаемый функцией setup()) и идет обращение к нему в шаблоне, Ref автоматически возвращает value. В шаблоне добавлять .value не нужно.

Вычисляемые свойства computed properties работают так же, внутри функции setup() обращаться к ним так .value.


REACTIVE


Мы только что рассмотрели применение ref() для задания реактивности свойств простых типов. А что будет, если мы хотим создать реактивный объект? Мы по прежнему могли бы использовать ref(), но под капотом Vue будет использовать reactive(), так что я буду придерживаться использования reactive().

С другой стороны reactive() не сработает с примитивными типами. Функция reactive() принимает объект и возвращает реактивный прокси оригинала. Это эквивалентно .observable() во Vue 2, и это имя функции было изменено во избежание путаницы с observables в RxJS.

import { reactive } from "vue";export default {  setup() {    const data = reactive({      title: "Hello, Vue 3"    });    return { data };  }};


Главное отличие в том, как мы обращаемся к реактивному объекту в шаблоне. В предидущем примере data это объект содержащий свойство title. Вам надо будет обращаться к нему в шаблоне так data.title:

<template>  <h1>{{ data.title }}</h1></template><script>  import { ref } from "vue";  export default {    setup() {      const data = ref({        title: "Hello, Vue 3"      });      return { data };    }  };</script>


Разница между REF и REACTIVE в компоненте


Исходя из того, что мы обсудили, ответ казалось бы напрашивается? Используем ref() для простых типов, и reactive() для объектов. Когда я начал создавать компоненты, выяснилось, что это всегда не так, и документации говорится:

Разница между применением ref и reactive может быть, в какой-то мере, сравнима с тем, как вы пишете стандартную логику программы на JavaScript.


Я размышлял над этой фразой и пришел к следующим выводам. По мере роста приложения у меня появились следующие свойства:

export default {  setup() {    const title = ref("Hello, World!");    const description = ref("");    const content = ref("Hello world");    const wordCount = computed(() => content.value.length);    return { title, description, content, wordCount };  }};


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

<template>  <div class="page">    <h1>{{ page.title }}</h1>    <p>{{ page.wordCount }}</p>  </div></template><script>  import { ref, computed, reactive } from "vue";  export default {    setup() {      const page = reactive({        title: "Hello, World!",        description: "",        content: "Hello world",        wordCount: computed(() => page.content.length)      });      return { page };    }  };</script>


Это мой подход к вопросу Ref или Reactive, но было бы не плохо узнать ваше мнение. Делаете-ли вы также? Может это не правильный подход? Пожалуйста комментируйте.

Логика композиции


Вы не сможете ошибиться при использовании ref() или reactive() в ваших компонентах. Обе функции создадут реактивные данные, и если вы понимаете как к ним обращаться в вашей функции setup() и в шаблонах не возникнет каких-либо сложностей.

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

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

import { ref, onMounted, onUnmounted } from "vue";export function useMousePosition() {  const x = ref(0);  const y = ref(0);  function update(e) {    x.value = e.pageX;    y.value = e.pageY;  }  onMounted(() => {    window.addEventListener("mousemove", update);  });  onUnmounted(() => {    window.removeEventListener("mousemove", update);  });  return { x, y };}


Если вы хотите использовать эту логику в компоненте и вызываете эту функцию, то деструктурируйте возвращаемый объект и затем возвращайте x и y координаты в шаблон.

<template>  <h1>Use Mouse Demo</h1>  <p>x: {{ x }} | y: {{ y }}</p></template><script>  import { useMousePosition } from "./use/useMousePosition";  export default {    setup() {      const { x, y } = useMousePosition();      return { x, y };    }  };</script>


Это будет работать, но если, поглядев на функцию, вы решили провести рефакторинг и возвращать объект position вместо x и y:

import { ref, onMounted, onUnmounted } from "vue";export function useMousePosition() {  const pos = {    x: 0,    y: 0  };  function update(e) {    pos.x = e.pageX;    pos.y = e.pageY;  }  // ...}


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

// компоте клиентexport default {  setup() {    // потеря реактивности!    const { x, y } = useMousePosition();    return {      x,      y    };    // потеря реактивности!    return {      ...useMousePosition()    };    // это единственный способ сохранить реактивность    // надо возвращать `pos` как есть, и обращаться в шаблоне к x и y так: `pos.x` и `pos.y`    return {      pos: useMousePosition()    };  }};


Но это не означает, что мы не можем в таком случае использовать reactive. Есть функция toRefs(), которая сконвертирует реактивный объект в простой объект, каждое свойство которого это ref соответствующего свойства оригинального объекта.

function useMousePosition() {  const pos = reactive({    x: 0,    y: 0  });  // ...  return toRefs(pos);}// x & y теперь ref!const { x, y } = useMousePosition();


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

ИТОГ


Когда я впервые начал использовать Composition API, я был озадачен вопросом, когда применять ref(), а когда reactive(). Возможно я до сих пор делаю это не правильно, и пока кто-нибудь не скажет мне этого, я буду придерживаться этого подхода. Надеюсь помог прояснить некоторые вопросы, и хотелось бы услышать ваше мнение.

Happy Coding
Подробнее..

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

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

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


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



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


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


Your browser does not support HTML5 video.

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



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


Установка


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


yarn add @nuxt/content

npm install @nuxt/content

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


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

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

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


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


mkdir content

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

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


mkdir content/articles

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


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

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


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

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

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


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


touch pages/blog/_slug.vue

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


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

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


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

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


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

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


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


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


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


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

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


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

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


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

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


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

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


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

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


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

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


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


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

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


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

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

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


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


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

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


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


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


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


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


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

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


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


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

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


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

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


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

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


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


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

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


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


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

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


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


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


export default {  components: true}

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


mkdir components/global

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


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

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


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

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



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


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


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

Подробнее..

Vue 3 на Typescript

07.11.2020 14:06:38 | Автор: admin

vue3+ts


Популярность Typescript растет день ото дня. Javascript нетипизированный язык(или слабо типизированный, если точнее), и одна и та же переменная способна принимать и строку, и число, и даже объект. С одной стороны, это делает язык гибким, с другой, потенциально ведет к многочисленным ошибкам. Typescript создан, чтобы решить эту проблему. Vue старается не отставать от моды, и в новой версии фреймворка была значительно улучшена поддержка языка. Теперь переход на Typescript проще и приятнее, чем был раньше. Хороший повод научиться чему-то новому, тем более, что в требованиях к вакансиям он встречается все чаще и чаще.


В этой статье мы перепишем тестовое задание, которое я разбирал ранее, на Vue 3 и Typescript и вдобавок используем обновленные Vue-Router и Vuex(критики, вы были услышаны).


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


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


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


npm update -g @vue/cli

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


vue create tsvue-todo-app

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


  • (*) TypeScript
  • (*) Router
  • (*) Vuex

Выбираем версию Vue для проекта: 3.x (Preview)


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


Ждем установки проекта


cd tsvue-todo-app

Открываем любимым редактором проект.


Удаляем все файлы из папок components, assets и views. Очищаем файл App.vue от кода.


Для начала займемся нашим хранилищем, которое находится в папке store. Его, как и все приложение, мы напишем на typescript. Моя статья, если это не было понятно из предисловия, рассчитана на тех, кто хорошо знаком с Vue и javascript, но почти ничего не знает о typescript.


Несколько слов о typescript. Его можно воспринимать как обертку или препроцессор для джаваскрипта, который проверяет код на соблюдение строгой типизации, то есть, что переменные, функции и объекты принимают только те типы данных, для которых и были задуманы, в остальном это обычный javascript. Соблюдена обратная совместимость, то есть вы можете писать на ts, как на js, и ваш код будет работать. Браузер, конечно же, не понимает typescript, для этого его нужно компилировать в javascript, который в итоге и идет в продакшен. К счастью, вам не нужно об этом беспокоиться, Vue CLI настроил все для вас. Достаточно запомнить особенности создания компонентов на typescript во Vue, для этого есть отличное руководство от самих создателей фреймворка(пока что доступно только на английском), и выучить типизацию данных в typescript, которая во Vue имеет свои особенности.

Существует множество способов создания типов в typescript. С полным спектром возможностей для наследования и инкапсуляции, которые так любят сторонники строго типизированных языков(таких как C# и Java). Мы пойдем по простому пути. Используем interface для создания модели объектов дел и списков дел. Создадим папку models в папке scr. Добавим файл ToDoModel.ts с кодом:


export default interface ToDo {  text: string;  completed: boolean;}

Теперь добавим файл NoteModel.ts:


import ToDo from './ToDoModel';export default interface Note {  title: string;  todos: Array<ToDo>;  id: number;}

Обратите внимание, что типы примитивов пишутся с маленькой буквы, они хоть и похожи, но отличаются от собратьев из js. Строка todos: Array<ToDo> означает массив, содержащий элементы типа ToDo, который мы импортировали из ToDoModel.ts


Теперь мы можем использовать эти модели в нашем приложении. Сначала займемся хранилищем. Оно должно находиться в src/store/index.ts. В первую очередь я советую установить строгий режим для хранилища. Дело в том, что данные в нем реактивные, как и во Vue, то есть, если вы просто используете их в компонентах, они будут автоматически меняться, в следствии локальных действий над ними. Это приведет к неразберихе в больших приложениях со множеством данных. Теперь мутации хранилища возможны только в самом хранилище, в противном случае будут вылетать ошибки. Импортируем наши модели. Создаем две переменные notes: [] as Note[],, будет содержать все заметки со списками задач(сразу определяем его тип с помощью typescript как массив записей), и currentNote, для создания/редактирования выбранной заметки. Создаем "небольшую" CRUD модель для них, и вот что должно получиться:


import { createStore } from 'vuex'import Note from '@/models/NoteModel'import ToDo from "@/models/ToDoModel"export default createStore({  state: {    notes: [] as Note[],    currentNote: {      title: "",      todos: [] as ToDo[],      id: 0    } as Note  },  mutations: {    addNote(state) {      state.notes.push(JSON.parse(JSON.stringify(state.currentNote)))    },    deleteNote(state) {      state.notes = state.notes.filter(note => note.id != state.currentNote.id)    },    updateNote(state) {      let note = state.notes.find(note => note.id === state.currentNote.id)      let index = state.notes.indexOf(note as Note)      state.notes[index] = JSON.parse(JSON.stringify(state.currentNote))    },    setCurrentNote(state, payload: Note) {      state.currentNote = JSON.parse(JSON.stringify(payload))    },    updateTitle(state, payload: string) {      state.currentNote.title = payload    },    updateTodos(state, payload: ToDo[]) {      state.currentNote.todos = payload    },    addNewTodo(state) {      state.currentNote.todos.push({        text: "",        completed: false      })    },    deleteTodo(state, index: number) {      state.currentNote.todos.splice(index, 1)    }  },  actions: {    saveNote({ commit }) {      const isOldNote: boolean = this.state.notes.some(el => el.id === this.state.currentNote.id)      if (isOldNote) {        commit('updateNote')      }      else {        commit('addNote');      }    },    fetchCurrentNote({ commit }, noteId: number) {      let note = this.state.notes.find(note => note.id === noteId)      commit('setCurrentNote', note)    },    updateCurrentNote({ commit }, note: Note) {      commit('setCurrentNote', note)    },  },  getters: {    getIdOfLastNote(state): number {      if (state.notes.length > 0) {        const index = state.notes.length - 1        return state.notes[index].id      } else {        return 0      }    }  },  strict: true})

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


А теперь перейдем к созданию первого компонента. Добавим файл ToDoItem.vue в папку components. Он нам пригодится для компонента создания/редактирования записи, и будет отвечать за отдельное дело.


<template>  <li>    <div>      <input v-model="checked" type="checkbox" />    </div>    <div>      <span        :class="{ completed: todo.completed }"        v-if="!editable"        @click="editable = !editable"      >        {{ todo.text ? todo.text : "Click to edit Todo" }}      </span>      <input        v-else        type="text"        :value="todo.text"        @input="onTextChange"        v-on:keyup.enter="editable = !editable"      />    </div>    <div>      <button @click="editable = !editable">        {{ editable ? "Save" : "Edit" }}      </button>      <button @click="$emit('remove-todo', todo)">Delete</button>    </div>  </li></template><script lang="ts">  import { defineComponent, PropType } from "vue"  import ToDo from "@/models/ToDoModel"  export default defineComponent({    name: "TodoItem",    props: {      todo: {        type: Object as PropType<ToDo>,        required: true      }    },    data() {      return {        editable: false      }    },    methods: {      onTextChange(e: { target: { value: string } }) {        this.$emit("update-todo", e.target.value)      }    },    computed: {      checked: {        get(): boolean {          return this.todo.completed        },        set(value: boolean) {          this.$emit("checkbox-click", value)        }      }    }  })</script><style scoped>  .completed {    text-decoration: line-through;  }</style>

Обратите внимание, что Vue компонент на typescript создается с помощью команды defineComponent. Всё внутреннее Options API должно быть вам знакомо. Единственно, что возникает путаница, при типизации props компилятор думает, что происходит операция присваивания. Нам пригодится объект PropType, который импортируется также из vue, он служит для указания типа объекта в typescript. В сложных ситуациях можно на месте указывать какое строение принимаемого объекта мы ожидаем onTextChange(e: { target: { value: string } }). Конечно, еще можно присваивать тип any, тогда переменная будет принимать все типы данных, но это убивает весь смысл typescript.


Строка this.$emit("update-todo", e.target.value) это просто вызов функции, которую передадут из внешнего компонента, и передача ей параметра e.target.value. Во внешней функции это будет выглядеть так:


      <TodoItem        <?--... -->        @update-todo="onUpdateTodo($event, index)"      />

Далее нам предстоит создание компонента Note.vue. Для интереса используем новый Composition API и функцию setup, все так же на typescript. Это усложняет работу, так как придется импортировать кучу вещей из vue, вот так:
import { defineComponent, computed, onMounted } from "vue". Наши $store и $router не будут доступны в функции setup, для доступа к ним их тоже придется импортировать. Мой обзор Composition API и работу с setup можно прочитать в моей прошлой статье. Напомню только, что функция setup вызывается до создания компонента и после получения props, хуки created и beforeCreate в ней не нужны, фактически это новый beforeCreate/created, все что вы писали в этих хуках, теперь нужно писать в setup. Что делает компонент Note.vue? Он используется для создания новой заметки и редактирования старых, для начала он связывает currentNote из хранилища с переменной note, потом он проверяет роута, если в нем присутствует :id, он просит хранилище найти нужную заметку в массиве заметок и записать её в currentNote, если нет, то устанавливает пустую заметку. А так же он содержит все логику для редактирования и сохранения заметок и отдельных дел.


<template>  <div>    <input :value="note.title" @input="updateTitle" />    <h2>{{ note.title }}</h2>    <hr />    <ul>      <TodoItem        v-for="(todo, index) in note.todos"        :todo="todo"        :key="index"        @remove-todo="onRemoveTodo(index)"        @update-todo="onUpdateTodo($event, index)"        @checkbox-click="onCheckboxClick($event, index)"      />    </ul>    <div class="new-todo">      <button @click="addNewTodo">        Add Todo      </button>      <span @click="addNewTodo">Add New Todo</span>    </div>    <hr />    <div>      <button @click="saveNote">        Save      </button>      <button @click="cancelEdit">        Cancel      </button>      <button @click="DeleteNote">        Delete      </button>    </div>    <hr />  </div></template><script lang="ts">  import TodoItem from "@/components/ToDoItem.vue"  import { defineComponent, computed, onMounted } from "vue"  import Note from "@/models/NoteModel"  import ToDo from "@/models/ToDoModel"  import store from "@/store"  import router from "@/router"  export default defineComponent({    name: "Note",    components: {      TodoItem    },    setup() {      const note = computed(() => store.state.currentNote)      const saveNote = () => {        store.dispatch("saveNote")        router.push("/")      }      const DeleteNote = () => {        store.commit("deleteNote", note)        router.push("/")      }      const { currentRoute } = router      const fetchNote = () => {        if (currentRoute.value.params.id) {          const routeId: number = +currentRoute.value.params.id          store.dispatch("fetchCurrentNote", routeId)        } else {          const id = store.getters.getIdOfLastNote + 1          store.commit("setCurrentNote", {            title: "",            todos: [] as ToDo[],            id: id          })        }      }      onMounted(fetchNote)      const updateTitle = (e: { target: { value: string } }) => {        store.commit("updateTitle", e.target.value)      }      const addNewTodo = () => {        store.commit("addNewTodo")      }      const onRemoveTodo = (index: number) => {        store.commit("deleteTodo", index)      }      const onUpdateTodo = (text: any, index: any) => {        let todos = JSON.parse(JSON.stringify(store.state.currentNote.todos))        todos[index].text = text        store.commit("updateTodos", todos)      }      const onCheckboxClick = (value: boolean, index: number) => {        let todos = JSON.parse(JSON.stringify(store.state.currentNote.todos))        todos[index].completed = value        store.commit("updateTodos", todos)      }      const cancelEdit = () => {        if (currentRoute.value.params.id) {          // undo changes        } else {        }        router.push("/")      }      const clearNote = () => {        const id = store.getters.getIdOfLastNote + 1        store.commit("setCurrentNote", {          title: "",          todos: [] as ToDo[],          id: id        } as Note)      }      return {        note,        saveNote,        addNewTodo,        cancelEdit,        onRemoveTodo,        onUpdateTodo,        DeleteNote,        clearNote,        updateTitle,        onCheckboxClick      }    },    beforeRouteLeave(to, from, next) {      this.clearNote()      next()    }  })</script><style>  .new-todo {    display: flex;    justify-content: flex-start;    background-color: #e2e2e2;    height: 36px;    margin: 5px 0px;    padding-top: 4px;    padding-left: 10px;    padding-right: 15px;    border-radius: 5px;  }</style>

Теперь логику компонента с помощью setup можно группировать для удобства чтения. Красота!


Вне функции setup вы заметили следующую функцию:


    beforeRouteLeave(to, from, next) {      this.clearNote()      next()    }

Это хук роутера, который вызывается каждый раз, когда юзер покидает страницу компонента. У нас он вызывает функцию "очистки" заметки.


Конечно, все это не будет работать без роутера. Вот какой код следует написать в \router\index.ts:


import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'import List from '@/views/List.vue'import Note from '@/views/Note.vue'const routes: Array<RouteRecordRaw> = [  {    path: '/',    name: 'List',    component: List  },  {    path: '/note',    name: 'Create',    component: Note  },  {    path: '/note/:id',    name: 'Edit',    component: Note  }]const router = createRouter({  history: createWebHistory(process.env.BASE_URL),  routes})export default router

А так же отредактируем файл App.vue:


<template>  <div id="app">    <nav>      <router-link class="router-link" to="/" exact>List Of Notes</router-link>      <router-link class="router-link" to="/note" exact        >Create Note</router-link      >    </nav>    <hr />    <router-view />  </div></template><script>  export default {    name: "App"  }</script>

И добавим файл List.vue в папку views:


<template>  <h2>List of Notes</h2>  <hr />  <div v-for="note in notes" :key="note.id" :to="`/note/${note.id}`">    <h2>{{ note.title }}</h2>    <h3>{{ note.id }}</h3>    <ul>      <li        :class="todo.completed ? 'completed' : ''"        v-for="(todo, index) in note.todos"        :key="index"      >        {{ todo.text }}      </li>    </ul>    <button @click="$router.push(`/note/${note.id}`)">Go to note</button>    <hr />  </div></template><script>  export default {    computed: {      notes() {        return this.$store.state.notes      }    }  }</script><style>  .completed {    text-decoration: line-through;  }</style>

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




Это не все тонкости работы с typescript на vue. Рекомендую также прочитать официальную документацию.


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

Подробнее..

Из песочницы vuex typescript vuexok. Велосипед, который поехал и обогнал всех

07.11.2020 22:21:21 | Автор: admin
Доброго времени суток.

Как и многие разработчики, я в свободное от работы время пишу свойотносительнонебольшой проект. Раньше писал на react, а на работе используется vue. Ну и что бы прокачаться во vue начал пилить свой проект на нем. Сначала всё было хорошо, прямо-таки радужно, пока я не решил, что надо бы еще прокачаться и в typescript. Так в моем проекте появился typescript. И если с компонентами всё былонеплохо, то с vuex всё оказалось печально. Так мне пришлось пройти все 5 стадий принятия проблемы, ну почти все.

Отрицание


Основные требования для стора:

  1. В модулях должны работать типы typescript
  2. Модули должно быть легко использовать в компонентах, должны работать типы для стейта, экшенов, мутаций и геттеров
  3. Не придумывать новое api для vuex, надо сделать так,чтобы как-то типы typescript заработали с модулями vuex, чтобы не приходилось разом переписывать всё приложение
  4. Вызов мутаций и экшенов должен быть максимально простым и понятным
  5. Пакет должен быть как можно меньше
  6. Не хочу хранить константы с именами мутаций и экшенов
  7. Оно должно работать (А как же без этого)

Не может быть что у такого уже зрелого проекта как vuex не было нормальной поддержки typescript. Ну-с, открываемGoogleYandex и погнали. Я был уверен на 100500% что с typescript всё должно быть отлично (как же я ошибался). Есть куча разных попыток подружить vuex и typescript. Приведу несколько примеров, которые запомнились, без кода чтобы не раздувать статью. Всё есть в документации по ссылкам ниже.

vuex-smart-module

github.com/ktsn/vuex-smart-module
Добротно, даже очень. Всё при себе, но лично мне не понравилось то, что для экшенов, мутаций, стейта, геттеров надо создавать отдельные классы. Это, конечно, вкусовщина, но это я и мой проект) И в целом вопрос типизации решен не до конца (ветка комментариев с объяснением почему).

Vuex Typescript Support

Хорошая попытка, но что-то много переписывать, да и вообще не принялось сообществом.

vuex-module-decorators

Казалось, что это идеальный способ подружить vuex и typescript. Похоже наvue-property-decorator, который я использую в разработке, работать с модулем можно как с классом, в общем супер, но

Но наследования нет. Классы модулей не корректно наследуются и issue на эту проблему висят уже очень давно! А без наследования будет очень много дублирования кода. Блин

Гнев


Дальше было совсем уже не очень, ну или так же идеального решения нет. Это тот самый момент, когда говоришь себе: Ну зачем я начал писать проект на vue? Ну ты же знаешь react, ну писал бы на react, там бы таких проблем не было! На основной работе проект на vue и тебе надо в нем прокачаться зашибись аргумент. А оно стоит потраченных нервов и бессонных ночей? Сиди как все, пиши компонентики, нет, тебе больше всех надо! Бросай этот vue! Пиши на react, прокачивайся в нем, за него и платят больше!

В тот момент я был готов хейтить vue как никто другой, но это были эмоции, и интеллект всё же был выше этого. Vue имеет (на мой субъективный взгляд) много преимуществ над react, но совершенства не бывает, как и победителей на поле сражений. И vue, и react по-своему хороши, а так как уже значительная часть проекта написана на vue, то было бы максимально глупо сейчас переходить на react. Надо было решить, что же делать с vuex.

Торг


Ну что же, дела обстоят не очень хорошо. Может тогда vuex-smart-module? Этот пакет вроде хорош, да, надо создавать много классов, но работает отлично же. Или может попробовать прописывать типы для мутаций и экшенов руками в компонентах и использовать чистый vuex? Там и vue3 c vuex4 на подходе, может у них дела с typescript обстоят лучше. Так что давай попробуем чистый vuex. И вообще на работу проекта это не влияет, всё же работает, типов нет, но вы держитесь. И держимся же)

Сначала так и начал делать, но код получается монструозный

Депрессия


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

Но если кому интересно, то код тут: (Наверное зря добавил этот фрагмент, но путь будет)

Слабонервным не смотреть
const getModule = <T>(name:string, module:T) => {  const $$state = {}  const computed: Record<string, () => any> = {}  Object.keys(module).forEach(key => {    const descriptor = Object.getOwnPropertyDescriptor(      module,      key,    );    if (!descriptor) {      return    }    if (descriptor.get) {      const get = descriptor.get      computed[key] = () => {        return get.call(module)      }    } else if (typeof descriptor.value === 'function') {      // @ts-ignore      module[key] = module[key].bind(module)    } else {      // @ts-ignore      $$state[key] = module[key]    }  })  const _vm = new Vue({    data: {      $$state,    },    computed  })  Object.keys(computed).forEach((computedName) => {    var propDescription = Object.getOwnPropertyDescriptor(_vm, computedName);    if (!propDescription) {      throw new Error()    }    propDescription.enumerable = true    Object.defineProperty(module, computedName, {      get() { return _vm[computedName as keyof typeof _vm]},      // @ts-ignore      set(val) { _vm[computedName] = val}    })  })  Object.keys($$state).forEach(name => {    var propDescription = Object.getOwnPropertyDescriptor($$state,name);    if (!propDescription) {      throw new Error()    }    Object.defineProperty(module, name, propDescription)  })  return module}function createModule<  S extends {[key:string]: any},  M,  P extends Chain<M, S>>(state:S, name:string, payload:P) {  Object.getOwnPropertyNames(payload).forEach(function(prop) {    const descriptor = Object.getOwnPropertyDescriptor(payload, prop)    if (!descriptor) {      throw new Error()    }    Object.defineProperty(      state,      prop,      descriptor,    );  });  const module = state as S & P  return {    module,    getModule() {      return getModule(name, module)    },    extends<E>(payload:Chain<E, typeof module>) {      return createModule(module, name, payload)    }  }}export default function SimpleStore<T>(name:string, payload:T) {  return createModule({}, name, payload)}type NonUndefined<A> = A extends undefined ? never : A;type Chain<T extends {[key:string]: any}, THIS extends {[key:string]: any}> = {  [K in keyof T]: (    NonUndefined<T[K]> extends Function       ? (this:THIS & T, ...p:Parameters<T[K]>) => ReturnType<T[K]>      : T[K]  )}


ПринятиеРождение велосипеда который обогнал всех. vuexok


Для нетерпеливых код тут, краткая документация тут.

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

Простейший модуль с vuexok выглядит так:

import { createModule } from 'vuexok'import store from '@/store'export const counterModule = createModule(store, 'counterModule', {  state: {    count: 0,  },  actions: {    async increment() {      counterModule.mutations.plus(1)    },  },  mutations: {    plus(state, payload:number) {      state.count += payload    },    setNumber(state, payload:number) {      state.count = payload    },  },  getters: {    x2(state) {      return state.count * 2    },  },})

Ну вроде почти как vuex, хотя что там на 10й строке?

counterModule.mutations.plus(1)

Воу! А это легально? Ну с vuexok да, легально) Метод createModule возвращает объект, который в точности повторяет структуру объекта модуля vuex, только без свойства namespaced, и мы можем использовать его для вызова мутаций и экшенов или для получения стейта и геттеров, причем все типы сохраняются. Причем из любого места, где его можно импортировать.

А что там с компонентами?

А с ними все отлично, так как фактически это vuex, то в принципе ничего не поменялось, commit, dispatch, mapState и т.д. работают как и раньше.

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

import Vue from 'vue'import { counterModule } from '@/store/modules/counterModule'import Component from 'vue-class-component'@Component({  template: '<div>{{ count }}</div>'})export default class MyComponent extends Vue {  private get count() {    return counterModule.state.count // type number  }}

Свойство state в модуле реактивно, как и в store.state, так что чтобы использовать состояние модуля в компонентах Vue достаточно просто вернуть часть состояния модуля в вычисляемом свойстве. Есть только одна оговорка. Я намеренно сделал стейт Readonly типом, не хорошо так стейт vuex изменять.

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

 private async doSomething() {   counterModule.mutations.setNumber(10)   // Аналогично вызову this.$store.commit('counterModule/setNumber', 10)   await counterModule.actions.increment()   // Аналогично вызову await this.$store.dispatch('counterModule/increment') }

Вот такая красота получилась. Чуть позже понадобилось еще реагировать на изменение jwt, который тоже хранится в сторе. И тогда появился у модулей метод watch. Вотчеры модулей работают также как store.watch. Единственная разница заключается в том, что в качестве параметров функции-гетера передаются стейт и гетеры модуля

const unwatch = jwtModule.watch(  (state) => state.jwt,  (jwt) => console.log(`New token: ${jwt}`),  { immediate: true },)

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

  1. типизированный стор есть
  2. типы работают в компонентах есть
  3. апи как у vuex и всё что было до этого на чистом vuex не ломается есть
  4. декларативная работа со стором есть
  5. маленький размер пакета (~400 байт gzip) есть
  6. не иметь необходимости хранить в константах названия экшенов и мутаций есть
  7. оно должно работать есть

Вообще странно что такой прекрасной фичи нет во vuex из коробки, это же офигеть как удобно!
Что касается поддержки vuex4 и vue3 не проверял, но судя по докам должно быть совместимо.

Так же решены проблемы представленные в этих статьях:

Vuex решаем старый спор новыми методами
Vuex нарушает инкапсуляцию

Влажные мечты:


Было бы здорово сделать так что бы в контексте экшенов были доступны мутации и другие экшены.

Как это сделать в контексте типов typescript хер его знает. Но если бы можно было делать так:

{  actions: {    one(injectee) {       injectee.actions.two()    },    two() {      console.log('tada!')    }}

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

Вот такое приключение с vuex и typescript. Ну, вроде выговорился. Спасибо за внимание.
Подробнее..

Цветовая палитра как часть дизайн-системы

07.06.2021 20:11:26 | Автор: admin

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

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

Характеристики цвета

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

Цветовые системы

Классификация цветовых систем:

  1. По свету (RGB). Смесь. Красный, зеленый, синий

  2. По краске (CMYK). Вычитание. Голубой, пурпурный, желтый, черный

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

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

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

Цвет света

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

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

Цветовой круг НьютонаЦветовой круг Ньютона

Иоганн Гёте смешал фиолетовый и красный, таким образом, получив пурпурный цвет и появился новый цветовой круг.

Цветовой круг Иоганна ГётеЦветовой круг Иоганна Гёте

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

Цветовой шар Отто РунгеЦветовой шар Отто Рунге

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

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

Цветовой круг ИттенаЦветовой круг Иттена

Мишель Шеврёль был первым, кто разработал цветовой атлас. В его основе 6 основных цветов в двенадцати модификациях.

Цветовая система ШеврёляЦветовая система Шеврёля

Цвет краски (Субтрактивный цвет или Subtractive color)

Эта система предсказывает спектр мощности света после прохождения света через последовательно поглощающие слои разных сред. Создателем цветового колеса при вычитаемом смешении является Моисей Харрис.

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

Цветовой круг Моисея ХаррисаЦветовой круг Моисея Харриса

Какой формат использует большинство?

Еще со времен CSS 2.1 принято использовать либо HEX, либо RGB-цвета. Недостатками использования такой формы представления цвета являются:

  • Система не понятна интуитивно. Мы не разделяем цвет отдельно на красный, зеленый и синий и не приводим цвет в шестнадцатеричную систему счисления, да и не говорим, например, Кремль цвета #ff0000.

  • Отсутствует поддержка. Дизайнерам может понадобиться 10 типов одного цвета, а в HEX и RGB нет никакой привязки к оттенкам.

HSL (hue, saturation, lightness)

Цвет в HSL представлении определяется тремя значениями:

  • тоном (оттенком, hue);

  • значением (светлотой, яркостью, value);

  • хромой (цветностью, насыщенностью, chroma, saturation).

Существует трехмерная колометрическая система Манселла. В ней цвет определяется с помощью трех координат.

Колометрическая система МанселлаКолометрическая система Манселла

Если составляющие цвета переименовать, мы получим следующую картину:

Выбор цветовой схемы

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

1. Выбор типа цветовой схемы

Итак, какие же бывают цветовые схемы, согласно теории цвета:

  • Монохромные (используется один основной цвет и его оттенки)

https://codepen.io/gevara2015/pen/xxVdooe

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

Всего же существует 5 цветовых схем:

  1. Монохроматическая схема (один основной цвет).

  2. Добавочная схема (два основных цвета).

  3. Триадная схема (три основных цвета).

  4. Тетраэдная схема (четыре основных цвета).

  5. Примыкающая схема (два или три основных цвета).

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

2. Именование переменных

Большинство именований переменных цвета, которые есть сейчас: accent, base, high, button-contrast-alpha, positive, negative, faint, success, warning... множество их. И почти к каждому обозначению есть вопросы.

  1. Возьмём, к примеру, high относительно чего он high, есть ли low, насколько high больше и по какому параметру low?

  2. Button-contrast-alpha. Если следовать логике этого именования, должен быть ряд alpha, beta, gamma. Опять же вопрос: чем они будут отличаться и в какой степени? Какой цвет будет иметь средние значения?

  3. Низкий уровень абстракции: button, text, link и т. д.

По именованию, мне кажется, нужно придерживаться ряда правил:

  • Вместо alpha, beta, gamma использовать strong, base, weak.

  • Primary (основной цвет бренда).

Учитывая множество абстракций в именовании (в имени переменной нельзя указывать конкретный параметр элемента разметки или сам элемент), я подумал о том, что было бы неплохо придерживаться именования по слоям (мало кто вспомнит аддон Tilt в Firefox). Чтобы отличить один элемент от другого, достаточно представить, что он находится просто уровнем выше. А чтобы он выделялся, нужно ему задать этот самый цвет подложки, что-то наподобие background и foreground.

Mozilla tilt addonMozilla tilt addon

И тут я наткнулся на объяснение принципов именования переменных для Material Design.

  1. Background (0dp elevation surface overlay)

  2. Surface (with 1dp elevation surface overlay)

  3. Primary

  4. Secondary

  5. On Background

  6. On Surface

  7. On Primary

  8. On Secondary

2.1 Разделение именований

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

https://codepen.io/gevara2015/pen/poywzLj

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

/* elements variables */--global-background: var(--bg);--surface: var(--bg-weak);--on-color: #fff;--on-background: var(--contrast-weak);--on-primary: var(--on-color);--on-secondary: var(--on-color);--on-error: var(--on-color);--on-surface-prime: var(--contrast-weak);--on-surface-second: var(--contrast-strong);--input-background: var(--bg-weak);--input-outline: inset 0 0 0 1px var(--secondary-strong);--component-outline: none;

Также стоит отметить, что для описания границ элемента лучше использовать не border, а box-shadow, так как он является более комплексным (можем применить сразу хоть три значения тени):

--component-outline: 15px 17px 26px -4px rgba(34, 60, 80, 0.6), 15px 17px 19px -11px rgba(20, 117, 191, 0.6), -22px -36px 19px -11px rgba(56, 167, 103, 0.6);

Плюс он не меняет размер всей кнопки или, к примеру, инпутау (если мы берем за исходные данные box-sizing: border-box). Мне кажется, что это является хорошим решением, так как разработчики описывают для каждого компонента набор переменных, а дизайнер в дальнейшем может играться с цветами или цветовыми схемами, с дизайнерскими подходами (skeuomorphism, neumorphism) или даже использовать LCH цвета, все на усмотрение дизайнера.

Lea Verou в своей статье указывает на вариант конвертации цветовой схемы для темной темы с помощью L (светлота) параметра в HSL формате. В итоге для светлой темы получается лесенка:

 --l-0: 0%;--l-30: 30%;--l-40: 40%;--l-50: 50%;--l-90: 90%;--l-100: 100%;

а для тёмной обратная лесенка переменных:

@media (prefers-color-scheme: dark) {:root {--l-0: 100%;--l-30: 70%;--l-40: 60%;--l-90: 10%;--l-100: 0%;}}

И казалось бы, все логично, можно просто определить текущий параметр светимости как формула 100% - lightness, и он будет средним как для темной, так и для светлой темы. Однако у hsl есть недостаток. В светлой теме будет недостаточно контраста для ряда элементов. Решение, предложенное Lea Verou c использованием LCH цветов, как мне кажется, еще слишком сырое, это все еще драфт в спецификациях w3c.

Выводы

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

  2. Настройка цветовой схемы лежит полностью на дизайнере, и он отдает всего лишь итоговый файл конфиг.

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

Автор: Александр Танцюра, Frontend Developer в Space307.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    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