В прошлой статье я сравнивал HappyX и Karax, показывая их декларативный подход к разработке с помощью Nim.

В этой статье я подробнее расскажу о разработке одностраничных приложений с помощью HappyX с использованием императивного подхода к разработке.

Обложка веб-фреймворка HappyX
Обложка веб-фреймворка HappyX

Так как в прошлой статье сравнивался лишь сам код, я начну с того, как установить HappyX. Для начала необходимо поставить сам Nim, если он у вас стоит — отлично.

Следующий шаг — установка HappyX с помощью пакетного менеджера nimble. Ставим последнюю версию:

nimble install happyx@#head

Отлично, можно проверить установленную версию с помощью

hpx -v

При желании можно поставить любую другую версию, например для установки версии 3.5.2 нужно ввести команду nimble install happyx@3.5.2

Теперь можно переходить к созданию проекта. Для этого достаточно перейти в нужную вам директорию и ввести следующую команду.

hpx create --kind HPX --name project --use-tailwind
  • --kind HPX — выбор типа проекта, в данном случае мы выбрали одностраничное приложение с поддержкой .hpx файлов.

  • --name project — выбор имени для проекта.

  • --use-tailwind — для одностраничного приложения мы также можем включить поддержку Tailwind CSS 3.

В созданном проекте можно наблюдать файл src/main.hpx (в исходный код добавлены комментарии):

<template>
  <div>
    <!-- Использование компонента HelloWorld -->
    <!--
      Передача параметров:
      userId как целое число,
      query и pathParam как строки
    -->
    <HelloWorld userId:int="10" query="meow" pathParam="Path Param Example"></HelloWorld>
  </div>
</template>

Также в файле src/components/HelloWorld.hpx можем видеть следующее:

<template>
  <div>
    Hello, world! {self.userId} <!-- Подстановка значений из текущего компонента -->
    <p>
      Query is
      {self.query}
    </p>
    <p>
      pathParam is
      {self.pathParam}
    </p>
  </div>
</template>


<!--
  Объявление скрипта, по желанию можно использовать здесь JavaScript,
  указав lang="javascript"
-->
<script>
# Здесь можно писать на Nim
echo "Hello, world!"

# Здесь задаются пропсы компонента, которые можно передавать из других компонентов
# Пропсам также можно задавать значение по умолчанию, как написано ниже
props:
  userId: int = 0
  query: string = ""
  pathParam: string = ""
</script>

<!-- Изолированная область стилей, применяемая только для данного компонента -->
<style>
  .div {
    border-radius: 4px;
    background-color: #212121;
    color: #ffffff;
  }
</style>

Помимо .hpx файлов в папке src находятся index.html и router.json. В первом нет ничего интересного кроме подключения скомпилированного js файла, а вот со вторым разберемся подробнее:

{
  "/": "main.hpx",
  "/user{ARG1:int}/{ARG2?:string}": {
    "component": "HelloWorld",
    "args": {
      "userId": "ARG1",
      "query": {
        "name": "q",
        "type": "query"
      },
      "pathParam": {
        "name": "ARG2",
        "type": "path"
      }
    }
  }
}

В файле router.json описаны маршруты вашего приложения, здесь вы можете изменять маршруты, задавать точки входа компонентов а также задавать параметры к маршрутам (query либо path).

Для того, чтобы запустить проект, достаточно находиться в папке с проектом (cd project). Теперь вы сможете наблюдать все изменения файлов в реальном времени.

hpx dev --reload

Попробуем изменить src/main.hpx следующим образом и сохранить файл:

<template>
  <div>
    <h1 h-if="self.showTitle()">
      Добро пожаловать!
    </h1>
    <h1 h-else>
      Скрытый заголовок
    </h1>
  </div>
</template>

<script>
<!-- Объявим метод компонента - showTitle -->
proc showTitle(): bool =
  true
</script>

В <template></template> можем наблюдать применение директив h-if и h-else, некие аналоги v-if и v-else из Vue.js. В <script></script> находится объявление метода showTitle, который возвращает true.

Результат измененного src/main.hpx
Результат измененного src/main.hpx

Как можно видеть - отображается лишь "Добро пожаловать!". Это происходит потому, что метод showTitle() всегда возвращает true.

Попробуем изменить скрипт:

<script>
import random

randomize()

proc showTitle(): bool =
  rand(100) > 50
</script>

Здесь мы добавили импорт стандартной библиотеки Nim — random, далее единожды вызываем функцию randomize(), теперь метод showTitle будет возвращать случайное значение. Попробуйте перезагрузить страницу несколько раз. Каждый раз надпись меняется!

Теперь попробуем реализовать собственный компонент, который будем использовать в src/main.hpx. Пусть этот компонент будет обычным счетчиком. Создадим файл src/components/Counter.hpx и запишем в него следующее:

<template>
  <div class="flex justify-around rounded-md border-2 border-gray-900 bg-gray-300 px-2 w-24">
    <div class="flex w-full self-center">{self.count}</div>
    <div class="flex flex-col text-sm">
      <button h-onclick="self.increase()">????</button>
      <button h-onclick="self.decrease()">????</button>
    </div>
  </div>
</template>

<script>
# Объявляем пропсы
props:
  count: int = 0

# Метод для увеличения счетчика
proc increase() =
  self.count += 1

# Метод для уменьшения счетчика
proc decrease() =
  self.count -= 1
</script>

В этом компоненте мы используем новую директиву — h-onclick. С помощью нее мы можем обращаться к методам либо писать какой‑то короткий Nim код.

Теперь обновим файл src/main.hpx, чтобы использовать в нем наш счетчик:

<template>
  <div>
    <h1 h-if="self.showTitle()">
      Добро пожаловать!
    </h1>
    <h1 h-else>
      Скрытый заголовок
    </h1>
    <div class="p-2">
      <Counter></Counter>
    </div>
  </div>
</template>

Как вы видите - достаточно лишь использовать тег <Counter></Counter>, чтобы использовать наш компонент. Также мы можем передать в него начальное значение счетчика:

<div class="p-2">
  <!--
    При передаче указываем, что это целое число,
    так как count в компоненте - целое число
  -->
  <Counter count:int="5"></Counter>
</div>

Ниже можно видеть результат, который у нас получился:

h-if и два счетчика
h-if и два счетчика

Добавим также верхний и нижний порог счетчика:

<script>
# Объявляем пропсы
props:
  count: int = 0
  floor: int = int.low
  ceil: int = int.high

# Метод для увеличения счетчика
proc increase() =
  if self.ceil <= self.count:
    return
  self.count += 1

# Метод для уменьшения счетчика
proc decrease() =
  if self.floor >= self.count:
    return
  self.count -= 1
</script>

Здесь мы добавили два пропса: ceil и floor, чтобы задать верхний и нижний порог соответственно. В начале методов мы добавили проверку, которая остановит функцию в случае достижения нижнего или верхнего порога.

Посмотрим, как это выглядит в src/main.hpx:

<div class="p-2">
  <Counter count:int="5" floor:nim="0" ceil:nim="10"></Counter>
</div>

Тут мы добавили передачу порогов с указанием :nim, с помощью которого тип автоматически распознается.


Маршрутизация

Теперь перейдем к маршрутизации, и, возвращаясь к src/router.json сразу добавим новый маршрут, по которому будет доступен только компонент с нашим счетчиком, при это мы сможем передавать ему значения.

{
  "/": "main.hpx",
  "/user{ARG1:int}/{ARG2?:string}": {
    "component": "HelloWorld",
    "args": {
      "userId": "ARG1",
      "query": {
        "name": "q",
        "type": "query"
      },
      "pathParam": {
        "name": "ARG2",
        "type": "path"
      }
    }
  },
  "/counter/{value:int}": {
    "component": "Counter",
    "args": {
      "count": "value"
    }
  }
}

Теперь попробуем зайти на http://localhost:5000/#/counter/10:

страница со счетчиком
страница со счетчиком

Попробуйте поиграться с адресной строкой — значение меняется.

Теперь добавим маршрут, в котором можно регулировать порог счетчика:

"/counter/{value:int}": {
  "component": "Counter",
  "args": {
    "count": "value"
  }
},
"/counter/{value:int}/{floor:int}-{ceil:int}": {
  "component": "Counter",
  "args": {
    "count": "value",
    "floor": "floor",
    "ceil": "ceil"
  }
}

Таким образом можно настраивать маршруты любой сложности и взаимодействовать с ними.

Директивы

В примерах выше уже были использованы директивы h-if и h-else, однако кроме них также есть ряд директив.

h-if, h-elif, h-else служат для условного рендеринга:

<template>
  <h1 h-if="5 > 10">5 больше 10</h1>
  <h1 h-elif="5 == 10">5 = 10</h1>
  <h1 h-else>5 меньше 10</h1>
</template>

директива h-for служит для цикличного рендеринга:

<template>
  <div>
    <div h-for="i in [1, 2, 3, 4]">
      {i}
      <div h-for="j in i..10">
        {i + j}
      </div>
    </div>
  </div>
</template>

Помимо h-for для цикличного рендеринга может использоваться h-while:

<template>
  <script lang="nim">
    # Здесь мы пишем на Nim
    var x = 1
  </script>
  <div class="flex">
    <div h-while="x <= 3">
      {x}
      <script lang="nim">
        # Здесь мы пишем на Nim
        x += 1
      </script>
    </div>
  </div>
</template>

Также существуют директивы для обработки событий. Они начинаются с h-on, например

  • h-onclick - обрабатывает нажатие мыши на элемент (click event);

  • h-onmouseover - срабатывает при наведении мыши на элемент (mouseover event);

  • h-onwheel - срабатывает при прокрутке колесика мыши (wheel event).

В каждом обработчике событий можно отправлять Event.

<template>
  <div>
    <button h-onclick="echo event.MouseEvent.x" h-onmouseover="echo event.MouseEvent.x">
      Click!
    </button>
    <input placeholder="edit text ..." h-oninput="echo event.target.value" />
  </div>
</template>

Заключение

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

Ссылки

Прошлая статья о сравнении HappyX и Karax.

GitHub репозиторий.

Официальный сайт с документацией, написанный на HappyX.

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


  1. hardtop
    04.12.2023 20:18
    +1

    Выглядит интересно. Но не могу понять, в чём изюминка. У ребят на js и так тьма своих фреймворков. Может, рассматривать как дополнение к python? Синтаксис у Нима схож, быстрый...


    1. akihayase Автор
      04.12.2023 20:18
      +1

      изюминка заключается в том, что вы можете разрабатывать одностраничные сайты не на JS, а на Nim :)


      1. hardtop
        04.12.2023 20:18
        +1

        Возможность избежать JS, конечно выглядит заманчивой. Но полностью отказаться от него сложно, потому как Листалки нужны, Анимации gsap, Графики динамические. Мы ж не будем переписывать - а просто возмём готовое.

        Хотя возможность не использовать Реакт - безусловное добро!


        1. akihayase Автор
          04.12.2023 20:18

          Отчасти согласен с вами, однако есть две вещи, которые позволяет Nim:
          1. Делать JS вставки напрямую в код, который выйдет после компиляции
          2. Взаимодействовать с JS напрямую через стандартную библиотеку jsffi, таким образом позволяя разработчику обращаться к любому JS объекту - вызывать функцию/метод, обращаться к переменной и так далее.

          Таким образом, вы можете использовать JavaScript, при этом остаетесь на Nim :)

          UPD:
          Далеко ходить не надо, ниже пример из официальной документации по JSFFI:

          import jsffi
          
          var console {.importc, nodecl.}: JsObject
          
          console.log("Hello, world!")