Перейти к содержанию

Знакомство с популярными JS фреймворками - Vue.js

Продолжаем обозревать известные фреймворки. И сегодня настала очередь Vue.js!

В этой серии⚓︎

Вступление⚓︎

Хорошая новость для тех, кто уже знаком с Alpine.js — синтаксис основных директив тут совпадает (потому что позаимствован разработчиком Alpine.js как раз таки у Vue.js и Angular). В большинстве случаев достаточно в вашем старом коде заменить x- на v- и считай полдела вы уже сделаете. Поэтому можно просто скопировать разметку из предыдущего проекта, с дальнейшей заменой директив, хотя всё-таки рекомендую работать с первоначальной вёрсткой.

С осени 2021 года синтаксис <script setup> стал рекомендуемым способом создания проектов на Vue 3. Поэтому мы изначально будем использовать новый синтаксис.

Vue.js⚓︎

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

npm create vite@latest vue-todo -- --template vue

Теперь перейдите в созданную папку vue-todo и установите tailwindcss:

npm i -D tailwindcss@next @tailwindcss/vite@next
pnpm create vite vue-todo --template vue

Теперь перейдите в созданную папку vue-todo и установите tailwindcss:

pnpm add -D tailwindcss@next @tailwindcss/vite@next
yarn create vite vue-todo --template vue

Теперь перейдите в созданную папку vue-todo и установите tailwindcss:

yarn add -D tailwindcss@next @tailwindcss/vite@next
bun create vite vue --template vue

Теперь перейдите в созданную папку vue-todo и установите tailwindcss:

bun add -D tailwindcss@next @tailwindcss/vite@next

и обновите vite.config.js:

import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue'
import tailwindcss from '@tailwindcss/vite';

// https://vitejs.dev/config/
export default defineConfig({
    plugins: [vue(), tailwindcss()],
});

В файл src/style.css замените всё содержимое на следующий код:

@import "tailwindcss";

Подготовка основных файлов⚓︎

Обновите файл index.html в корне проекта:

<!DOCTYPE html>
<html lang="ru">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Список дел</title>
  </head>
  <body class="bg-gray-200">
    <div id="app"></div>
    <script type="module" src="/src/main.js"></script>
  </body>
</html>

Откройте файл src/App.vue и замените его содержимое таким образом:

<script setup>
  import TodoList from './components/TodoList.vue';
</script>

<template>
  <TodoList title="Список дел" />
</template>

При использовании <script setup> импортированные компоненты автоматически становятся доступными для шаблона.

Файл src/components/HelloWorld.vue можно удалить, он нам не понадобится.

Осталось запустить dev-сервер и начать создавать компоненты:

npm run dev
pnpm run dev
yarn dev
bun run dev

Компонент TodoList⚓︎

Создадим файл src/components/TodoList.vue:

<script setup>
  import { ref } from 'vue';

  // Объявляем входные параметры
  defineProps({
    title: String,
  });

  // Создаем переменную-ссылку для хранения списка задач
  const todos = ref([]);
</script>

<template>
  <div class="max-w-sm md:max-w-lg mx-auto my-10 bg-white rounded-md shadow-md overflow-hidden">
    <h1 class="text-2xl font-bold text-center py-4 bg-gray-100">{{ title }}</h1>
    <ul class="list-none p-4" v-show="todos.length"></ul>
  </div>
</template>

Обратите внимание на директиву v-show в разметке. С её помощью мы будем отображать список ul только если массив todos не пуст. В противном случае элемент будет скрываться с помощью CSS display: none.

Примечание

При именовании своих компонентов всегда используйте имена из нескольких слов. Это связано с рекомендациями Vue.js

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

<script setup>
  import { ref, onMounted } from 'vue';

  // ...

  const fetchTodos = async () => {
    await fetch('https://dummyapi.online/api/todos')
      .then((response) => response.json())
      .then((data) => {
        // У переменных ссылок все значения должны записываться в свойство value
        todos.value = data.slice(0, 10);
      });
  };

  // Выполняем функцию fetchTodos во время монтировани компонента
  onMounted(fetchTodos);

  // ...
</script>

Как видите, во Vue хук onMounted это аналог директивы x-init (в Alpine.js). Теперь при обновлении страницы будет загружаться список дел с JSON-сервера.

Далее реализуем метод для добавления новой задачи:

const addTodo = (title) => {
  if (!title) return;

  todos.value = [
    ...todos.value,
    {
      id: crypto.randomUUID(),
      title: title,
      completed: false,
    },
  ];
};

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

const toggleTodo = (id) => {
  todos.value = todos.value.map((t) =>
    t.id === id ? { ...t, completed: !t.completed } : t,
  );
};

const deleteTodo = (id) => {
  todos.value = todos.value.filter((todo) => todo.id !== id);
};

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

Компонент TodoForm⚓︎

Итак, создадим файл src/components/TodoForm.vue:

<script setup>
  import { ref } from 'vue';

  const emit = defineEmits(['submit']);

  // Создаем переменную-ссылку на элемент input с атрибутом v-model="input"
  // Теперь получить доступ к его значению можно через input.value
  const input = ref('');

  // Создаем переменную-ссылку на DOM-элемент input с атрибутом ref="newTodo" в разметке
  const newTodo = ref(null);

  // Вообще-то можно было бы не создавать переменную input и работать со значением текстового элемента через newTodo.value.value, но выглядит не очень красиво

  const addTodo = () => {
    // Отправляем из дочернего компонента (то есть отсюда) в родительский введённое значение из элемента `input`, связывая его с событием `submit`
    emit('submit', input.value);

    // Очищаем форму ввода
    input.value = '';

    // И фокусируемся на ней (мало ли, вдруг ещё одну задачу захотим сразу ввести)
    newTodo.value.focus();
  };
</script>

<template>
  <div class="p-4 bg-gray-100">
    <div class="flex items-center">
      <input
        ref="newTodo"
        v-model="input"
        type="text"
        class="flex-1 mr-2 py-2 px-4 rounded-md border border-gray-300"
        placeholder="Новая задача"
        autofocus
      />
      <button
        class="bg-blue-500 hover:bg-blue-600 text-white py-2 px-4 rounded-md"
        @click="addTodo"
      >
        Добавить
      </button>
    </div>
  </div>
</template>

Теперь компонент TodoList можно обновить таким образом:

<script setup>
  import { ref, onMounted } from 'vue';
  import TodoForm from './TodoForm.vue';

  // ...
</script>

<template>
  <div class="max-w-sm md:max-w-lg mx-auto my-10 bg-white rounded-md shadow-md overflow-hidden">
    <h1 class="text-2xl font-bold text-center py-4 bg-gray-100">{{ title }}</h1>
    <ul class="list-none p-4" v-show="todos.length"></ul>
    <TodoForm @submit="addTodo" />
  </div>
</template>

Компонент TodoItem⚓︎

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

Создадим файл src/components/TodoItem.vue:

<script setup>
  // Определяем входные параметры
  const props = defineProps({ todo: Object });

  // Определяем события, которые будем отправлять в родительский компонент
  const emit = defineEmits(['toggle', 'remove']);

  const toggleTodo = () => emit('toggle', props.todo.id);

  const deleteTodo = () => emit('remove', props.todo.id);
</script>

<template>
  <li class="flex items-center mb-2 hover:cursor-pointer" @click="toggleTodo">
    <input type="checkbox" class="mr-2" :checked="todo.completed" />
    <span :class="{ 'line-through': todo.completed }" v-text="todo.title"></span>
    <div class="ml-auto">
      <button class="text-gray-400 hover:text-gray-600" @click="deleteTodo">
        <svg
          xmlns="http://www.w3.org/2000/svg"
          fill="none"
          viewBox="0 0 24 24"
          stroke-width="1.5"
          stroke="currentColor"
          class="w-6 h-6"
        >
          <path
            stroke-linecap="round"
            stroke-linejoin="round"
            d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"
          />
        </svg>
      </button>
    </div>
  </li>
</template>

Примечание

Во Vue разметка <span v-text="variable"></span> аналогична <span>{{ variable }}</span>. Результат отображения будет одним и тем же.

Теперь можно вызывать компонент TodoItem в качестве элемента списка внутри тега ul, не забыв прикрепить к нему соответствующие атрибуты:

<script setup>
  import { ref, onMounted } from 'vue';
  import TodoItem from './TodoItem.vue';
  import TodoForm from './TodoForm.vue';

  // ...
</script>

<template>
  <div class="max-w-sm md:max-w-lg mx-auto my-10 bg-white rounded-md shadow-md overflow-hidden">
    <h1 class="text-2xl font-bold text-center py-4 bg-gray-100">{{ title }}</h1>
    <ul class="list-none p-4" v-show="todos.length">
      <TodoItem
        v-for="todo in todos"
        :key="todo.id"
        :todo="todo"
        @toggle="toggleTodo"
        @remove="deleteTodo"
      />
    </ul>
    <TodoForm @submit="addTodo" />
  </div>
</template>

Гораздо больше кода, по сравнению с одним файлом в Alpine.js, не правда ли? Но зато разложено всё по кусочкам и каждый компонент можно заменить альтернативным или использовать отдельно. Например, можно создать AnotherTodoForm с теми же входными параметрами и методами, что и в TodoForm, но с другой HTML-разметкой, и задействовать его в TodoList вместо TodoForm.

Ниже на странице можно найти ссылки на готовый проект сразу в двух версиях — на устаревшем Options API и на современном Composition API. Можете открыть и сравнить синтаксис, если кому интересно.

Документация⚓︎

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

Заключение⚓︎

Итак, мы закончили наше простое приложение TODO на Vue.js:

  • успешно мигрировали с Alpine.js, узнав сходства и различия с Vue
  • познакомились с основными директивами
  • создали несколько компонентов

В следующей части этой серии мы познакомимся с React.


Комментарии