Организованная разработка фронтенда должна отвечать на три вопроса:

  1. как делать компоненты

  2. как производить ререндер при изменении состояния

  3. как работать с CSS-стилями

И казалось бы библиотека, отвечающая на три таких вопроса может быть простой и давно закончена так как сами HTML и JS давно не претерпевали больших изменений. Единственное, однозначно новой вехой можно назвать появление TypeScript, как возможность следовать строгой типизации, но очевидно это не ограничивающих фактор.

Однако история фронтенд-разработки богата технологиями и различными парадигмами, "вершиной" которых на данный момент можно назвать реактивность и html-like шаблоны. Все это можно найти в доминирующих технологиях вроде React, Angular и Vue, которые, как ни странно, "развиваются" до сих пор (Angular пробил 22 версии). Мы не будем надолго останавливаться на их проблемах, назовем только одну – сложна, в разных смыслах. А мы не любим сложности в разработке простых вещей и поэтому появился LandTS.

Что такое LandTS

https://github.com/pain64/land-ts

LandTS это библиотека для разработки фронтендов на TypeScript. Весь LandTS это state-of-art файл land.ts (177 строк кода) и плагин к vite (100 строк кода), хотя это может быть любой бандлер. Ядро библиотеки это всего лишь две функции l (land) и lz (land zone for rerender). Буква l выбрана так как она имеет наименьший синтаксический шум, после этого мы выбрали слово land, которое имеет удачный перевод "приземлить". Приведем базовый пример:

import { css, l } from "./land.ts";

document.body.appendChild(
  l('div', _ => {
    css`
        color: #005143;
    `.applyTo(_)
    _.innerText = 'Hello from Land.ts!'
  })
)

Функция l

l имеет несколько перегрузок.

const aDiv: HTMLDivElement = 
  l('div', newDiv => { 
    // this lambda is __init__ for newDiv
    newDiv.innerText = 'Hello, from Land.ts!'
  })

В данном случае первый аргумент это html-tag, а второй это init-лямбда элемента.

l('div', _ => {
  l(_, 'p', _ => { _.innerText = 'LandTS №1' })
  l(_, 'p', _ => { _.innerText = 'React №2' })
})

А при таком вызове функции l, первым аргументом передается часть DOM-дерева, куда нужно вмонтировать новый элемент. Заметим, что JavaScript позволяет использовать переиспользовать имя переменной _ и мы получаем минимум синтаксического шума.

Асинхронность

В случае, если создаваемый элемент имеет асинхронный init, LandTS сделает плейсхолдер, в который будет вмонтирован элемент, когда будет получен init

l('div', _ => {
  l(_, 'p', _ => { _.innerText = 'paragraph 1' })
  l(_, 'p', async _ => {
    await someAsyncInvokation()
    _.innerText = 'paragraph 2'
  })
  l(_, 'p', _ => { _.innerText = 'paragraph 3' })
})

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

Делается очень просто, вообще с LandTS вы всю логику пишете на TypeScript

const root = l('div', _ => {
  const tasks = [
    { isImportant: false, description: 'Go for a walk' },
    { isImportant: true,  description: 'Repair the TV' }
  ]

  for (task of tasks)
    l(_, 'div', _ => {
      _.innerText = task.description
      
      if (task.isImportant) {
        css`color: red;`.applyTo(_)
        _.innerText += "!!!!!!!!!!!!!"
      }
    })
})

Программирование стилей, элементов, контента и т.д. Удобно! Не правда ли?

Компоненты

Компонент в LandTS это просто класс, который должен реализовать один из интерфейсов Component и AsyncComponent с одним методом

interface Component<H extends HTMLElement> {
    render(): H
}
interface AsyncComponent<H extends HTMLElement> {
    render(): Promise<H>
}

Почему их два? Потому что разработчики JS решили что должен явно существовать Promise. Других причин нет, возможно стоит оставить один интерфейс, но пока так. Сделаем и смонтируем свой компонент

class Button implements Component<HTMLButtonElement> {
  render(): HTMLButtonElement {
    return l('button', _ => {
      // ...
    })
  }
}

// usage
l('div', _ => {
  l(_, new Button())
})

В данном случае в вызове функции l нет init лямбды так как init находится в конструкторе нашего класса.

Функция lz и ререндеринг

LandTS предлагает простой и удобный способ делать ререндеринг "РУКАМИ" ?, хотя и не останавливает вас от прямого управления DOM-деревом.

Каждая функция l имеет брата-близнеца lz, которая расширяет init-функцию вторым параметром z (rerender zone). Также нам понадобится функция sync

function sync(zones: Array<Zone | null>, changesReason?: any): void

Мы используем функцию sync чтобы синхронизировать локальное состояние с DOM-деревом, посредством вызова ререндера (функцией sync) на одной или нескольких зонах (Zone).

l('div', _ => {
  let counter = 0
  lz(_, 'div', (_, z) => { // will be rerendered fully
    l(_, 'span', _ => { _.innerText = 'Current count: ' + count })

    l(_, 'button', _ => {
      _.innerText = 'click to increment'
      _.onclick = () => sync([z], counter++) // triggers rerender
    })
  })
})

Более того, вы можете делать ререндер раздельно

l('div', _ => {
  let z1 = lz(_, 'p', _ => { _.innterText = 'zone 1' })
  let z2 = lz(_, 'p', _ => { _.innterText = 'zone 2' })

  l(_, 'button', _ => {
    _.onclick = () => {
      sync([z1])
      // or
      sync([z2])
    }
  })
})

CSS

Итак, вы можете настраивать стили отдельно в своих css-файлах, а можете использовать функцию css`` и стилизовать разметку локально

// project_root/src/Button.ts
import { css, l } from "./land.ts";

class Button implements Component<HTMLButtonElement> {
  render(): HTMLButtonElement {
    return l('button', _ => {
        css`
            font-size: 16px;
        `.applyTo(_)
    })
  }
}

Во время сборки проекта для каждого .ts файла будет создан .css файл. Для каждого вызова функции css`` будет сгенерирован вызов конструктора Css-класса

/* project_root/.css/src/Button.ts.css */
.g1 {
    font-size: 16px;
}

А вызов функции в файле .ts будет трансформирован перед загрузкой

// project_root/src/Button.ts after transform
    return l('button', _ => {
        new Css('g1')
        
          .applyTo(_)
    })

Используя .this-class селектор вы можете встраивать любой css-код

class SectionHeader implements ClassComponent<HTMLDivElement> {
  render(): HTMLDivElement {
    return l('h2', _ => {
      css`
        @media (max-width: 550px) {
          .this-class {
            text-align: center;
          }
        }
      `.apply(_)
    })
  }
}

->

@media (max-width: 550px) {
  .g2 {
    text-align: center;
  }
}

Это буквально всё что есть в LandTS :)

Порт игры крестики-нолики

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

class GameExample implements ClassComponent<HTMLDivElement> {
    render(): HTMLDivElement {
        let currentMove = 0
        const history: Array<Array<string>> = [
            ['', '', '', '', '', '', '', '', ''],
        ]

        function isWinnerContested() {
            const winPatterns = [
                [0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6],
                [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6],
            ]
            const board = history[currentMove]

            for (let [a, b, c] of winPatterns)
                if (board[a] != '' && board[a] === board[b] && board[a] === board[c])
                    return true

            return false
        }

        function currentPlayer() { return currentMove % 2 == 1 ? 'O' : 'X' }


        return lz('div', (_, z) => {
            l(_, 'div', _ => {
                l(_, 'div', _ => {
                    _.innerText = (isWinnerContested() ? 'Winner: ' : 'Next player: ')
                        + currentPlayer()
                })
                l(_, 'div', _ => {
                    css`
                        display: grid;
                        grid-template-rows: repeat(3, 32px);
                        grid-template-columns: repeat(3, 32px);
                    `.applyTo(_)

                    const board = history[currentMove]

                    for (let i = 0; i < 9; i++) {
                        l(_, 'button', _ => {
                            _.innerText = board[i]
                            _.onclick = () => {
                                if (board[i] != '' || isWinnerContested()) return

                                sync([z], (() => {
                                    history.push(board.toSpliced(i, 1, currentPlayer()))
                                    currentMove++
                                })())
                            }
                        })
                    }
                })
            })
            l(_, 'ol', _ => {
                for (let i = 0; i < history.length; i++)
                    l(_, 'li', _ => {
                        l(_, 'button', _ => {
                            _.innerText = 'Go to ' + (i == 0 ? 'game start' : 'move #' + i)
                            _.onclick = () => { sync([z], currentMove = i) }
                        })
                    })
            })
        })
    }
}

Скачав код из репозитория вы можете убедиться сами, что он работает безупречно быстро.

Используйте LandTS, ставьте звездочки https://github.com/pain64/land-ts!

Также есть видео с подробным обзором https://www.youtube.com/watch?v=x-whF7gJhOI

Спасибо!

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