Tic Tac Toe, часть 0: Сравнение Svelte и React

Tic Tac Toe, часть 0: Сравнение Svelte и React
Tic Tac Toe, часть 1: Svelte и Canvas 2D
Tic Tac Toe, часть 2: Undo/Redo с хранением состояний
Tic Tac Toe, часть 3: Undo/Redo с хранением команд
Tic Tac Toe, часть 4: Взаимодействие с бэкендом на Flask с помощью HTTP

На сайте 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}
        <Square />
    {/each}
</div>

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

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

Square.svelte
<button></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 стили. Показано использование вложенных компонентов: компонент Square импортируется в компонент Board, компонент Board импортируется в компонент App. Показано использована блока each в компоненте Board. Стили меняются редко, поэтому разместил их после html разметки, чтобы лишний раз не пролистывать их.


Passing Data Through Props

ReactSvelte
В Square объявлено свойство value.


<script>
    export let value = '';
</script>

<button>{value}</button>

В Board показано использование индексов массива для заполнения клеток.


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

Making an Interactive Component

ReactSvelte
По клику в клетке появляется крестик. В Square добавлен обработчик события DOM handleClick для клика мышкой. Добавлена переменная state для отображения крестика в клетке.


<script>
    export let value = '';
    let state = '';

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

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

Lifting State Up

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


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

    let state = {
        squares: Array(9).fill(''),
    };

    function handleClick(i) {
        const squares = state.squares.slice();
        squares[i] = 'X';
        state.squares = squares;
    }
</script>

<div class="status">Next player: X</div>
<div class="board">
    {#each state.squares as value, i}
        <Square {value} on:click={e => handleClick(i)}/>
    {/each}
</div>

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

<button on:click>
    {value}
</button>

Taking Turns

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


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

    let state = {
        squares: Array(9).fill(''),
        xIsNext: true,
    };

    function handleClick(i) {
        const squares = state.squares.slice();
        squares[i] = state.xIsNext ? 'X' : 'O';
        state.squares = squares;
        state.xIsNext = !state.xIsNext;
    }   
</script>

<div class="status">Next player: {state.xIsNext ? 'X' : 'O'}</div>
<div class="board">
    {#each state.squares as value, i}
        <Square {value} on:click={e => handleClick(i)}/>
    {/each}
</div>

Declaring a Winner

ReactSvelte
Добавлена функция определения победителя calculateWinner в отдельном файле helper.js. Запрещен клик по уже установленным клеткам и после победы.


heplers.js
export function calculateWinner(squares) {
    const lines = [
        [0, 1, 2],
        [3, 4, 5],
        [6, 7, 8],
        [0, 3, 6],
        [1, 4, 7],
        [2, 5, 8],
        [0, 4, 8],
        [2, 4, 6],
    ];
    for (let i = 0; i < lines.length; i++) {
        const [a, b, c] = lines[i];
        if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
            return squares[a];
        }
    }
    return null;
}  

Board.svelte
<script>
    import Square from './Square.svelte';
    import { calculateWinner } from './helpers.js';

    let state = {
        squares: Array(9).fill(''),
        xIsNext: true,
    };

    $: winner = calculateWinner(state.squares);

    function handleClick(i) {
        if (winner || state.squares[i]) 
            return;

        const squares = state.squares.slice();
        squares[i] = state.xIsNext ? 'X' : 'O';
        state.squares = squares;
        state.xIsNext = !state.xIsNext;
    }   
</script>

<div class="status">
    {#if winner}
        <b>Winner: {winner}</b>
    {:else}
        Next player: {state.xIsNext ? 'X' : 'O'}
    {/if}
</div>
<div class="board">
    {#each state.squares as value, i}
        <Square {value} on:click={e => handleClick(i)} />
    {/each}
</div>

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


Репозиторий на GitHub

https://github.com/nomhoi/tic-tac-toe


Установка игры на локальном компьютере:


git clone git@github.com:nomhoi/tic-tac-toe.git
cd tic-tac-toe
npm install
npm run dev

Запускаем игру в браузере по адресу: http://localhost:5000/.


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


UPDATE2: Добавлен репозиторий туториала на GitHub.

Поделиться публикацией

Комментарии 22

    +6
    Спасибо за статью! Рад что интерес к 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
      Ваш вариант не рабочий.
      «0» не поставить, победитель не определяется
        0
        Блин, в примере, который я правил не было нуля. Только крестники ставились.))) Короче не последняя версия видимо. Исправил пример на скорую руку. Можно конечно и еще проще написать. Спасибо что заметили!
          0
          const _squares = squares.slice();
          _squares[i] = xIsNext ? 'X' : 'O';
          squares = _squares;

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


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


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

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

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

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

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

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

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

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

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


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

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


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

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

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

                    Это же мини-демо проектик. Тащить в него immer или immutablejs для «читаемой» иммутабильности, имхо, было бы лишним. Проще написать 2 дополнительных строчки на чистом js, понятные всем. Пассаж про обычный js был про это.

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

                    Окей, я понял. Ни разу не являюсь фанатом redux, поэтому ничего не предлагаю. Говорю о том, что можете юзать в Svelte что хотите.

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

                    Раз не пробовали, тогда поверьте тем, кто пробовали. Юзаем immer на одном проекте. Вот переписал пример с его использованием: REPL

                    Всё таки это компилятор и надо понимать что получится в итоговом js-коде, какие будут обёртки и стоит ли игра свеч.

                    Ничего особенного там в этом смысле не получается. Единственное правило — Svelte должен «увидеть» присвоение в стейт. Можно считать что = это автоматический вызов setState. Если подробнее:

                    // не будет перерисовки, даже если immutable=false
                    arr.push(item);
                    
                    // будет перерисовка, если immutable=false
                    arr.push(item);
                    arr = arr; // триггер дла set state
                    
                    // НЕ будет перерисовка, если immutable=true
                    arr.push(item);
                    arr = arr;
                    
                    // будет перерисовка, если immutable=true
                    arr = [ ...arr, item ];
                    

                    Последний вариант самый правильный и стоит юзать immutable=true. Как именно вы будете делать иммутабильность для Svelte не важно. В случае с immer:

                    arr = produce(arr, _arr => _arr.push(item));
                    
                    // скомпилируется всего лишь в это:
                    
                    $$invalidate('arr', arr = produce(arr, _arr => _arr.push(item)));
                    
                      +1

                      Спасибо за примеры.

              0
              Если мы собираемся записывать каждое состояние игры в историю, то необходимо полное копирование массива. Если история не нужна, то можно и не создавать копию.
          +3
          Спасибо за подсказку! В ближайшие дни я исправлю примеры и обновлю статью.
            +2
            А зачем в 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')}`;
            })();
            


            И было бы круто примеры кода сразу в статье видеть. Утомляет каждый раз по ссылке переходить.
              +2
              $: 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 для этого.
                0
                Понятно, а если еще добавить статус ничьи?
                  0
                  это еще один статус\состояние игры. Победитель, ничья, следующий ход.

                  я бы так делал:
                  {#if state === 'победа' }
                    ...
                  {:else if state === 'ничья'}
                    ...
                  {:else if state === 'ход'}
                    ...
                  {/if}
                  
                    +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
                Да, beforeUpdate можно было бы и не вводить. Кстати, в туториале для React'a статус тоже почему-то определяется в методе render.

                Я, думаю, сделаю новую редакцию статьи и там размещу код. Здесь как, принято ли новую редакцию статьи в виде отдельной статьи оформлять? Если исправить прямо здесь, то новым читателям уже не будут понятны старые комментарии.
                  0
                  Лучше исправить текущую. Делать еще одну статью с правками, но тем же смыслом не стоит.
                    0
                    Хорошо, понятно.
                +1
                Начал исправлять статью и исходный код в соответствии с замечаниями. Пока еще не до конца.
                  0
                  Добавил репозиторий туториала на GitHub .

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

                  Самое читаемое