На сайте React'a есть туториал, в котором описывается разработка игры Tic Tac Toe. Я решил повторить разработку этой игры на Svelte. Статья охватывает только первую половину туториала, до реализации истории ходов. Для целей ознакомления с фреймворком этого вполне достаточно. Каждый раздел статьи соответствует разделу туториала, содержит ссылки на исходный код обоих фреймворков.


Inspecting the Starter Code

ReactSvelte


App.svelte
<script>
    import Board from './Board.svelte';
</script>

<div class="game">
    <div class="game-board">
        <Board />
    </div>
    <div class="game-info">
        <div></div>
        <ol></ol>
    </div>
</div>

<style>
    .game {
        font: 14px "Century Gothic", Futura, sans-serif;
        margin: 20px;
        display: flex;
        flex-direction: row;
    }

    .game-info {
        margin-left: 20px;
    }

    ol {
        padding-left: 30px;
    }
</style>

Board.svelte
<script>
    import Square from './Square.svelte';
</script>

<div class="status">Next player: X</div>
<div class="board">
    {#each Array(9) as square, i}
        <Square value={i}/>
    {/each}
</div>

<style>
    .board { 
        width: 102px; 
    }

    .status { 
        margin-bottom: 10px; 
    }
</style>

Square.svelte
<script>
    export let value = '';
    let state = '';

    function handleClick() {
        state = 'X';
    }
</script>

<button on:click={handleClick}>
    {state}
</button>

<style>
    button {
        background: #fff;
        border: 1px solid #999;
        float: left;
        font-size: 24px;
        font-weight: bold;
        line-height: 34px;
        height: 34px;
        margin-right: -1px;
        margin-top: -1px;
        margin-bottom: -1px;
        padding: 0;
        text-align: center;
        width: 34px;
    }
    button:focus { 
        outline: none; 
    }
</style>

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


Passing Data Through Props

ReactSvelte
Объявлено свойство value в Square. В Board показано использование индексов массива для заполнения клеток.


Making an Interactive Component

ReactSvelte
По клику в клетке появляется крестик. В Square добавлен обработчик события DOM.


Lifting State Up

ReactSvelte
До этого момента состояние клеток хранилось в них самих, сейчас они переведены в один массив, который размещен в компоненте Board, т.е. в Board сейчас хранится состояние всей игры. Обработчик клика handleClick перенесен в компонент Board. Square теперь снова отображает состояние клетки с помощью свойства value.


Taking Turns

ReactSvelte
Добавлено появление нолика после крестика.


Declaring a Winner

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


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


UPDATE: Исправлена статья и исходные коды в соответствии с замечаниями в комментариях.

Комментарии (19)


  1. PaulMaly
    17.06.2019 23:44
    +1

    Спасибо за статью! Рад что интерес к Svelte ростет.))

    Несколько ремарок:

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

    Svelte умеет ровно также:

    <button class="square" on:click={() => handleClick(i)}>...</button>
    

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

    <button class="square" on:click>...</button>
    


    Далее можно ловить клик прямо на Square и вообще не передавать i в Square:

    <Square value={square} on:click={e => handleClick(i)}/>
    


    Заранее извиняюсь, но позволил себе переписать ваш пример немного более рационально.


    1. DeniSun
      18.06.2019 09:31
      +1

      Ваш вариант не рабочий.
      «0» не поставить, победитель не определяется


      1. PaulMaly
        18.06.2019 09:34

        Блин, в примере, который я правил не было нуля. Только крестники ставились.))) Короче не последняя версия видимо. Исправил пример на скорую руку. Можно конечно и еще проще написать. Спасибо что заметили!


      1. faiwer
        19.06.2019 20:40

        const _squares = squares.slice();
        _squares[i] = xIsNext ? 'X' : 'O';
        squares = _squares;

        Подскажите, а в чём суть этого финта с копией массива. Если написать это более простым способом явно поменяв по ключу напрямую — так не сработает?


        Попробовал сам так:


        squares[i] = xIsNext ? 'X' : 'O';

        кажется всё работает.


        1. PaulMaly
          19.06.2019 22:04

          Иммутабельность же. Помогает всем фреймворка и Svelte в том числе лучше «понимать» что изменилось в объекте. В Svelte есть специальная опция компилятора immutable: true, которую мы обычно используем в проектах. И всем советую. Собственно с этой опцией ваш способ работать не будет.


          1. faiwer
            20.06.2019 00:25

            Понятно. И immer какой-нибудь наверное сюда уже не подключишь, Svelte ведь компилятор :( После стандартных redux-их {… } простыней видеть эти костыли в Svelte, конечно, неприятно.


            1. PaulMaly
              20.06.2019 02:11

              C immer прекрасно работает. Не понимаю какое вообще отношение к этому имеет компиляция.

              После стандартных redux-их {… } простыней видеть эти костыли в Svelte, конечно, неприятно.

              Так используйте {… } никто также не мешает. Как вы обеспечиваете иммутабильностью Svelte вообще не волнует. Для реактивности важен только факт присвоения значения, а иммутабильность не обязательна и работает внутри. Флагом immutable: true вы лишь говорите Svelte — «тебе не стоит парится на объектами, если ссылка на них не изменилась».

              И было бы интересно узнать что из примера выше вы считаете «костялем»? Вроде бы обычный js.


              1. faiwer
                20.06.2019 11:45

                И было бы интересно узнать что из примера выше вы считаете «костялем»?

                Очевидно же — две строки из 3-х. Натужная иммутабельность, которая плохо сказывается на читаемости, написании и поддержки кода. Пассаж про обычный js не понял, обычный js не запрещает писать костыли :)


                Так используйте {… } никто также не мешает

                Гхм… Вы меня наверное недопоняли. От них после redux уже тошно, а вы мне их ещё и в svelte тащить предлагаете? :) Я собственно потому и упомянул immer.


                C immer прекрасно работает. Не понимаю какое вообще отношение к этому имеет компиляция.

                Ну тут надо пробовать. Я пока близко в Svelte не присматривался. Насколько я понимаю, он выискивает все мутабельные операции и добавляет к ним явные setter-ы. Так что в случае immer тут будет всё зависеть от того, сможет ли он подхватить их, будет ли там callback, как на него отреагирует svelte. Нужно ли будет помещать такой блок в $: {} кодовый блок. Как я и написал выше — надо пробовать и смотреть что получится… Всё таки это компилятор и надо понимать что получится в итоговом js-коде, какие будут обёртки и стоит ли игра свеч.


        1. nomhoi Автор
          20.06.2019 04:41

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


  1. nomhoi Автор
    18.06.2019 14:07

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


  1. afrokick
    18.06.2019 16:56

    А зачем в beforeUpdate каждый раз проверять статус? Если можно при изменении перерасчет сделать.

    Svelte: reactive declarations

    $: winner = calculateWinner(state.squares);
    $: status = winner ? `Winner: ${winner}` : `Next player: ${(state.xIsNext ? 'X' : 'O')}`;
    //или
    $: status = (() => {
      const winner = calculateWinner(state.squares);
      return winner ? `Winner: ${winner}` : `Next player: ${(state.xIsNext ? 'X' : 'O')}`;
    })();
    


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


    1. PaulMaly
      19.06.2019 09:06

      $: status = (() => {
      const winner = calculateWinner(state.squares);
      return winner? `Winner: ${winner}`: `Next player: ${(state.xIsNext? 'X': 'O')}`;
      })();

      Воу, зачем так сложно)) Это же не JSX какой-то, а обычны JS (по крайней мере синтаксически ;-) ). Можно делать блочные реактивные декларации:

      let status;
      $: {
        const winner = calculateWinner(squares);
        status = winner ? `Winner: ${winner}` : `Next player: ${xIsNext ? 'X' : 'O'}`;
      }
      

      Но в целом, я бы не стал увлекаться конкатенацией строк в скриптах и полностью перевел бы это дело в шаблон. На случай если вывод статуса нужно будет как-то дополнительно задизайнить, например `Winner: ${winner}` выводить жирным:

      <div class="status">
      {#if winner}
        <b>Winner: {winner}</b>
      {:else}
        Next player: {xIsNext ? 'X' : 'O'}
      {/if}
      </div>
      

      Все таки в Svelte у нас html-first и прекрасный DSL для этого.


      1. nomhoi Автор
        19.06.2019 15:09

        Понятно, а если еще добавить статус ничьи?


        1. afrokick
          19.06.2019 15:16

          это еще один статус\состояние игры. Победитель, ничья, следующий ход.

          я бы так делал:

          {#if state === 'победа' }
            ...
          {:else if state === 'ничья'}
            ...
          {:else if state === 'ход'}
            ...
          {/if}
          


          1. PaulMaly
            19.06.2019 16:25
            +1

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

            <script>
              ...
              $: winner = calculateWinner(squares);
              $: draw = ! squares.includes('') && ! winner;
              ...
            </script>
            
            <div class="status">
            {#if winner}
              Winner: {winner}
            {:else if draw}
              Draw!
            {:else}
              Next player: {xIsNext ? 'X' : 'O'}
            {/if}
            </div>
            


            Обновил мой пример. Тот же draw пригождается еще и для отмены click()


    1. nomhoi Автор
      19.06.2019 15:08
      +1

      Да, beforeUpdate можно было бы и не вводить. Кстати, в туториале для React'a статус тоже почему-то определяется в методе render.

      Я, думаю, сделаю новую редакцию статьи и там размещу код. Здесь как, принято ли новую редакцию статьи в виде отдельной статьи оформлять? Если исправить прямо здесь, то новым читателям уже не будут понятны старые комментарии.


      1. afrokick
        19.06.2019 15:12

        Лучше исправить текущую. Делать еще одну статью с правками, но тем же смыслом не стоит.


        1. nomhoi Автор
          19.06.2019 15:17

          Хорошо, понятно.


  1. nomhoi Автор
    20.06.2019 16:03

    Начал исправлять статью и исходный код в соответствии с замечаниями. Пока еще не до конца.