Перестаньте усложнять веб-разработку — попробуйте Svelte

Svelte был признан самым любимым веб-фреймворком разработчиков в 2021 году (ссылка). Так что же такое Svelte и почему его так любят?

Svelte — это довольно уникальный javascript-фреймворк, цель которого — быть «по-настоящему реактивным» и помочь разработчикам «писать меньше кода».

Он обладает удивительной особенностью создавать крошечные пучки кода, заставляя сам фреймворк «исчезать» на этапе компиляции, поставляя оптимизированный ванильный код JavaScript вместо использования больших и сложных библиотек, загружаемых во время выполнения.
На мой взгляд, именно эта особенность выгодно отличает Svelte от других JavaScript-фреймворков. Крошечный размер пакета означает гораздо более быстрое время загрузки, что, похоже, является направлением развития Интернета, поскольку все больше и больше данных показывают преимущества быстрого веб-сайта. Этот этап компиляции также устраняет необходимость в использовании таких техник, как виртуальный DOM, используемый в React и Vue, что еще больше увеличивает скорость работы сайта.

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

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


Оглавление

  1. Установка
  2. Особенности компонента
  3. Переменные & Реактивность
  4. onMount & Async Fetching
  5. Реактивные объявления
  6. Циклы
  7. Условный рендеринг
  8. Компоненты и реквизиты
  9. Пользовательские события
  10. Переадресация привязки
  11. Хранилища
  12. Заключительные заметки

Установка

Сначала установим базовый Svelte. Для этого выполните следующую команду

npx degit sveltejs/template new-svelte-project.

Это скопирует стартовый шаблон svelte в нужную вам папку.
Чтобы включить typescript, перейдите в новую папку svelte и выполните команду

node scripts/setupTypeScript.js

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

npm install.

Особенности компонентов

Компонент svelte — это файл, заканчивающийся на .svelte.
Как вы можете видеть в App.svelte, компонент Svelte может быть довольно простым, содержащим три части: html, тег script для размещения javascript и тег style для размещения css.
Это похоже на Vue, только без шаблонного кода.

Очистим содержимое скрипта и html в App.svelte, а в теге style используем заданный css.

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

Переменные создаются в теге script.
Мы можем создать строковую переменную и отобразить ее в DOM очень просто, используя фигурные скобки {}.

<!-- For example -->
<script lang="ts">
  let name: string = 'pokemon searcher'
</script>

<h1>{name}</h1>
Вход в полноэкранный режим Выход из полноэкранного режима

Для поиска по именам покемонов нам понадобится поле ввода и переменная, которая будет хранить содержимое этого поля.
Чтобы сделать переменную ‘pokemonName’ равной содержимому поля ввода, мы можем использовать специальное легкое ключевое слово ‘bind’, которое обеспечивает двустороннее связывание переменной ‘pokemonName’.

<!-- App.svelte -->
<script lang="ts">
  let pokemonName: string = ''
</script>

<main>
  <span>Search: </span>
  <input type="text" bind:value="{pokemonName}" />

  <h1>Pokemon: {pokemonName}</h1>
</main>
Вход в полноэкранный режим Выход из полноэкранного режима

Теперь ввод в поле ввода изменяет вывод названия покемона.
Это ключевое слово bind обеспечивает двустороннее связывание без использования функции ‘onInput’, которая изменяет значение переменной ‘pokemonName’, как в React.

onMount & Async Fetching

В этом примере приложения мы храним имена покемонов из pokeapi в переменной в виде массива строк.

Мы хотим получить данные и отобразить их сразу после рендеринга компонента.
Для этого мы можем использовать ‘onMount’, которая представляет собой простую функцию жизненного цикла, запускаемую после первого отображения компонента в DOM. Давайте используем ее для получения данных из pokeapi и отображения их в массив имен покемонов.

<!-- App.svelte - script tag -->
<script lang="ts">
  import { onMount } from 'svelte'

  let pokemonName: string = ''

  // Fetch from api then store the mapped names.
  let pokemonData: string[] = []
  onMount(() => {
    const setPokemonData = async (): Promise<void> => {
      const rawPokemonData = await (
        await fetch('https://pokeapi.co/api/v2/pokemon?limit=99')
      ).json()

      pokemonData = rawPokemonData.results.map(
        (p: { name: string; url: string }) => p.name
      )
    }
    setPokemonData()
  })
</script>
Вход в полноэкранный режим Выход из полноэкранного режима

Теперь у нас есть список имен покемонов в массиве ‘pokemonData’, который мы можем использовать в нашем простом проекте.

Реактивные объявления

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

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

$: reactiveVar = otherVar * 2;
Войти в полноэкранный режим Выход из полноэкранного режима

Итак, ‘reactiveVar’ — это переменная, но ее значение вычисляется каждый раз, когда изменяется переменная ‘otherVar’ (svelte выполняет вычисления, когда изменяются переменные, используемые в этих вычислениях).
Мы можем сделать переменную, которая хранит отфильтрованные имена покемонов, реактивным объявлением. Мы назовем ее ‘suggestions’.

<!-- App.svelte - bottom of script tag -->
<script>
  // ...

  let suggestions: string[]
  $: suggestions = 
       pokemonName.length > 0
         ? pokemonData.filter(
             (name) => name.includes(pokemonName)
           )
         : pokemonData
</script>
Вход в полноэкранный режим Выход из полноэкранного режима

Итак, ‘suggestions’ — это массив имен покемонов, который включает в себя строку, введенную в поле ввода.
Реактивное присвоение не работает с typescript, поэтому мы можем объявить переменную ‘suggestions’ обычным образом, чтобы сохранить проверку типов.

Циклы

Мы хотим отобразить содержимое массива ‘suggestions’ на странице, и мы можем сделать это с помощью цикла svelte. В Svelte есть специальное ключевое слово ‘each’, которое позволяет нам отображать элементы DOM для каждого элемента в заданной итерации.
Чтобы отобразить имя каждого покемона, просто используйте ключевое слово each для перебора переменной ‘pokemonData’.

<!-- App.svelte - html -->
<main>
  <span>Search: </span>
  <input type="text" bind:value="{pokemonName}" />

  {#each suggestions as suggestion}
    <h2>{suggestion}</h2>
  {/each}
</main>
Вход в полноэкранный режим Выход из полноэкранного режима

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

Условный рендеринг

В Svelte есть и другие ключевые слова. Еще одно полезное — #if.
Ключевое слово #if позволяет использовать условную логику.
Например, мы можем отобразить экран загрузки, пока получаем данные из pokeapi.

<!-- App.svelte - html -->
<main>
  {#if pokemonData && pokemonData.length > 0}
    <span>Search: </span>
    <input type="text" bind:value="{pokemonName}" />

    {#each suggestions as suggestion}
      <h2>{suggestion}</h2>
    {/each}
  {:else}
    <h2>Loading...</h2>
  {/if}
</main>
Вход в полноэкранный режим Выход из полноэкранного режима

Компоненты и реквизиты

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

<script lang="ts">
  export let stringProp: string
</script>
Вход в полноэкранный режим Выход из полноэкранного режима

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

<script lang="ts">
  import NewComponent from './NewComponent.svelte'
</script>

<NewComponent  stringProp="prop value"  />
Вход в полноэкранный режим Выход из полноэкранного режима

Для нашего приложения давайте создадим компонент для каждого предложения.
Создайте новый файл ‘Suggestion.svelte’ в src/, и просто примите и отобразите реквизит ‘suggestion’.

<!-- Suggestion.svelte -->
<script lang="ts">
  export let suggestion: string
</script>

<div class="suggestion">{suggestion}</div>

<style>
    .suggestion {
        font-size: 1.25rem;
    }
</style>

Вход в полноэкранный режим Выход из полноэкранного режима

Теперь мы можем импортировать этот компонент и использовать его в нашем цикле #each.

<!-- App.svelte - top of script tag -->
<script lang="ts">
  import Suggestion from './Suggestion.svelte'

  // ...
  // ...
</script>
Вход в полноэкранный режим Выход из полноэкранного режима
<!-- App.svelte - html -->
<main>
  {#if pokemonData && pokemonData.length > 0}
    <span>Search: </span>
    <input type="text" bind:value="{pokemonName}" />

    {#each suggestions as suggestion}
      <Suggestion suggestion="{suggestion}"/>
    {/each}
  {:else}
    <h2>Loading...</h2>
  {/if}
</main>
Войти в полноэкранный режим Выход из полноэкранного режима

Сейчас это довольно бессмысленно, поэтому давайте добавим немного логики в компонент ‘Suggestion’ в виде событий.

Пользовательские события

Пользовательские события могут передаваться от одного компонента к другому. Это позволяет нам иметь связь между родителями и детьми.
Для нашего приложения мы хотим иметь возможность нажать на предложение, чтобы выбрать нашего покемона. Мы можем сделать это, отправив пользовательское событие из компонента ‘Suggestion’ в компонент App, а затем установив значение переменной, которая содержит выбранного нами покемона.

Сначала создайте новую переменную ‘selectedPokemon’ и отобразите ее на экране в App.svelte.

<!-- App.svelte - bottom of script tag -->
<script lang="ts">
  // ...
  // ...

  let chosenPokemon: string = ''
</script>
Вход в полноэкранный режим Выход из полноэкранного режима
<!-- App.svelte - html -->
<main>
  {#if pokemonData && pokemonData.length > 0}
    <h1>Chose Your Pokemon</h1>
    <h2>Chosen Pokemon: {chosenPokemon}</h2>

    <div>
      <span>Search: </span>
      <input type="text" bind:value="{pokemonName}" />

      {#each suggestions as suggestion}
        <Suggestion suggestion="{suggestion}"/>
      {/each}
    </div>
  {:else}
    <h2>Loading...</h2>
  {/if}
</main>
Войдите в полноэкранный режим Выйти из полноэкранного режима

Теперь в Suggestion.svelte мы можем отправлять пользовательское событие ‘chosePokemon’ при нажатии на предложение.
Чтобы создать пользовательское событие, нам нужно импортировать ‘createEventDispatcher’ из svelte.

<!-- Suggestion.svelte - script tag -->
<script lang="ts">
  import { createEventDispatcher } from 'svelte'

  export let suggestion: string

  // Function to dispatch a custom event.
  const dispatch = createEventDispatcher()
  const chosePokemon = (): void => {
    dispatch('chosePokemon', {
      pokemon: suggestion
    })
  }
</script>
Вход в полноэкранный режим Выход из полноэкранного режима

Теперь у нас есть функция ‘chosePokemon’, которая отправляет пользовательское событие ‘chosePokemon’ родительскому компоненту.

Чтобы вызвать эту функцию при нажатии на предложение, нам нужно использовать событие svelte ‘on:click’ следующим образом.

<!-- Suggestion.svelte - html -->
<div class="suggestion" on:click="{chosePokemon}">
  {suggestion}
</div>
Вход в полноэкранный режим Выход из полноэкранного режима

Вернувшись в файл App.svelte, мы можем обработать это пользовательское событие, используя синтаксис ‘on:(имя события)’.

<!-- App.svelte - 'Suggestion' component in html -->
<Suggestion
  suggestion="{suggestion}"
  on:chosePokemon="{(e) => {
    chosenPokemon = e.detail.pokemon
  }}"
/>
Войти в полноэкранный режим Выйти из полноэкранного режима

Этот обработчик устанавливает значение переменной selectedPokemon равным имени покемона, переданному в пользовательском событии (находится в свойстве ‘detail’).
Когда мы щелкаем на предложении, отображается имя этого покемона.

Я установил переменную ‘selectedPokemon’ таким образом, чтобы ввести пользовательские события, однако есть гораздо более чистый и простой способ сделать это: переадресация привязки.

Переадресация привязки

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

В App.svelte мы можем заменить обработчик события chosePokemon ключевым словом bind на реквизите selectedPokemon.

<!-- App.svelte - 'Suggestion' component in html -->
<Suggestion suggestion="{suggestion}" bind:chosenPokemon />
Вход в полноэкранный режим Выход из полноэкранного режима

А в компоненте ‘Suggestion’ мы можем принять этот реквизит и сделать так, чтобы функция ‘on:click’ просто устанавливала эту переменную ‘chosePokemon’.

<!-- Suggestion.svelte -->
<script lang="ts">
  export let suggestion: string
  export let chosenPokemon: string = ''
</script>

<div 
  class="suggestion" 
  on:click="{() => chosenPokemon = suggestion}"
>
  {suggestion}
</div>
Вход в полноэкранный режим Выход из полноэкранного режима

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

Магазины

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

По сути, магазин — это объект с методом subscribe, который позволяет нашим компонентам Svelte получать уведомления о любых изменениях в значении магазина. Svelte определяет 2 различных типа хранилищ: записываемое и читаемое. Как следует из названия, записываемое хранилище позволяет читать и записывать, а читаемое хранилище позволяет только читать.

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

Сначала нам понадобится новый файл в папке src (я назову его ‘stores.ts’).
Мы можем импортировать функцию readable store из svelte, а также необходимые типы.

// stores.ts
import { readable, Readable, Subscriber } from 'svelte/store'
Вход в полноэкранный режим Выход из полноэкранного режима

Функция readable в качестве первого аргумента принимает начальное значение магазина, а в качестве второго аргумента — функцию ‘start’.
Эта функция ‘start’ вызывается, когда магазин получает первого подписчика, поэтому именно здесь мы будем получать наши api данные.
Функция получает функцию обратного вызова ‘set’, которая используется для установки значения магазина, и возвращает функцию ‘stop’, которая вызывается, когда последний подписчик отписывается от магазина (где мы можем выполнить некоторую очистку).

В нашем магазине мы можем просто скопировать содержимое нашей функции ‘setPokemonData’, но вместо присвоения значения ‘pokemonData’ мы вызываем функцию ‘set’.

import { readable, Readable, Subscriber } from 'svelte/store'

const setPokemonData = 
  async (set: Subscriber<string[]>): Promise<void> => {
    const rawPokemonData = await (
      await fetch('https://pokeapi.co/api/v2/pokemon?limit=99')
    ).json()

    set(
      rawPokemonData.results.map(
        (p: { name: string; url: string }) => p.name
      )
    )
  }

// Export the new store 'pokemonData' variable.
export const pokemonData: Readable<string[]> = 
  readable([], (set) => {
    setPokemonData(set)

    return () => set([])
  })

Вход в полноэкранный режим Выход из полноэкранного режима

Вот и все. Теперь у нас есть центральное хранилище имен наших покемонов в ‘pokemonData’.

Чтобы использовать наш магазин, нам нужно импортировать переменную ‘pokemonData’ из нашего файла магазина.
Затем мы можем использовать специальный символ ‘$’ для ссылки на значение магазина.

<!-- App.svelte -->
<script lang="ts">
  import { pokemonData } from './stores.js'
  import Suggestion from './Suggestion.svelte'

  let pokemonName: string = ''

  let suggestions: string[]
  // $pokemonData instead of pokemonData
  $: suggestions =
    pokemonName.length > 0
      ? $pokemonData.filter((name) => 
          name.includes(pokemonName)
        )
      : $pokemonData

  let chosenPokemon: string = ''
</script>

<main>
  {#if $pokemonData && $pokemonData.length > 0}
    <h1>Chose Your Pokemon</h1>
    <h2>Chosen Pokemon: {chosenPokemon}</h2>

    <div>
      <span>Search: </span>
      <input type="text" bind:value="{pokemonName}" />
      {#each suggestions as suggestion}
        <Suggestion 
          suggestion="{suggestion}" 
          bind:chosenPokemon 
        />
      {/each}
    </div>
  {:else}
    <h2>Loading...</h2>
  {/if}
</main>
Вход в полноэкранный режим Выход из полноэкранного режима

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

Теперь, хотя в svelte есть записываемые и читаемые хранилища, все, что придерживается «контракта» svelte store и реализует метод subscribe, является хранилищем.
Это означает, что магазины очень гибкие и могут быть адаптированы к вашим потребностям. Вы даже можете создать хранилище на другом языке, например, на Rust, как показано здесь.

Заключительные замечания

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

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

Команда Svelte сейчас работает над SvelteKit, который представляет собой версию Next.js от Svelte, о которой вы можете узнать здесь.


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

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *