Как создать веб-приложение, если вы пишите на Nim? Что такое HappyX и как можно создать на нем приложение для заметок? Обо всем этом вы узнаете в полной статье.

Обложка статьи
Обложка статьи

Здравствуйте! Меня зовут Никита, и сегодня я хочу рассказать вам о создании веб-приложений на языке программирования Nim.

Коротко о Nim

Начнем с простого. Nim - это язык программирования, который может компилироваться в C, C++, Objective C и в JavaScript. Благодаря этому мы можем писать как серверные приложения, так и клиентские.

Если у вас не установлен Nim, установите его, следуя инструкции на официальном сайте.

Что такое HappyX?

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

Хорошо, а дальше что?

Серверная часть

Разрабатывать мы ее будем все на том же HappyX с использованием примитивной sqlite, представленной нам библиотекой Nim.

Сперва необходимо поставить библиотеки happyx и db_connector. Делается это с помощью пакетного менеджера nimble:

nimble install happyx@#head
nimble install db_connector

После установки библиотек создаем проект с помощью happyx cli:

hpx create --name server --kind SSR --language Nim

Далее во время выполнения команды выбираем don't use templates и создаем клиентский проект:

hpx create --name client --kind SPA --language Nim -u

Теперь переходим в /server/src/main.nim и импортируем необходимые библиотеки:

import
  happyx,
  db_connector/db_sqlite

Затем создадим модели запроса для создания и редактирования наших заметок:

# Модель запроса
# Ниже мы будем обрабатывать ее в виде JSON
model CreateNote:
  # единственное обязательное поле string
  name: string

model EditNote:
  completed: bool

Далее объявляем наше серверное приложение:

# Задаем хост и порт
serve "127.0.0.1", 5000:
  # Преднастройка сервера в gcsafe (garbage collector safe) области
  setup:
    # Подключаем базу данных
    var db = open("notes.db", "", "", "")

    # Создаем таблицу, если она не существует
    db.exec(sql"""
    CREATE TABLE IF NOT EXISTS notes(
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      name VARCHAR(50) NOT NULL,
      completed INTEGER NOT NULL DEFAULT 0
    );
    """)

Пока что ничего сложного, верно?

Создадим POST запрос на создание заметки:

serve "127.0.0.1", 5000:
  setup:
    ...

  # Объявляем POST запрос для создания новой заметки
  post "/note[note:CreateNote]":
    # Выведем название заметки
    echo note.name
    # Вставляем заметку в базу данных и получаем ее ID
    let id = db.insertId(sql"INSERT INTO notes (name) VALUES (?)", note.name)
    # Возвращаем ID заметки в ответе
    return {"response": id}

И запускаем наше приложение:

nim c -r server/src/main

Пробуем сделать запрос:

Результат POST запроса на создание заметки
Результат POST запроса на создание заметки

Взглянем на нашу таблицу:

Содержимое таблицы notes
Содержимое таблицы notes

Отлично! Что дальше?

Теперь попробуем сделать GET запрос на получение всех заметок:

  post "/note[note:Note]":
    ...

  # GET запрос для получения всех заметок
  get "/notes":
    # Список заметок:
    var notes = %*[]
    # Пробегаемся по всем строчкам:
    for row in db.rows(sql"SELECT * FROM notes"):
      # Добавляем новый элемент в список
      notes.add %*{"id": row[0].parseInt, "name": row[1], "completed": row[2] != "0"}
    return {"response": {
      "items": notes,
      "size": notes.len
    }}

Взглянем на Postman:

Результат выполнения GET запроса на получение всех заметок
Результат выполнения GET запроса на получение всех заметок

Наконец, создадим PATCH запрос:

  get "/notes":
    ...

  # PATCH запрос на изменение заметки по ее ID
  patch "/note/{noteId:int}[note:EditNote]":
    # Смотрим, есть ли такая заметка вообще
    var row = db.getRow(sql"SELECT * FROM notes WHERE id = ?", noteId)
    # заметка не найдена - возвращаем ошибку
    if row[0] == "":
      statusCode = 404
      return {"error": "заметка с таким ID не найдена"}
    # Обновляем нашу заметку
    db.exec(sql"UPDATE notes SET completed = ? WHERE id = ?", note.completed, noteId)
    # И возвращаем успешный статус
    return {"response": "success"}

И снова смотрим Postman:

Успешный результат выполнения PATCH запроса
Успешный результат выполнения PATCH запроса
Ошибочный результат выполнения PATCH запроса
Ошибочный результат выполнения PATCH запроса

Круто! Что теперь?

На этом разработка серверной части окончена. Теперь мы перейдем к написанию клиентской части.

Клиентская часть

Здесь мы переходим в client/src/main.nim и редактируем его. Для начала разберемся с импортом:

import
  happyx,
  std/strformat,  # форматирование строк
  std/jsfetch,  # fetch
  std/asyncjs,  # async
  std/sugar,  # синтаксический сахар
  std/httpcore,  # HTTP методы
  std/json  # работа с JSON

Теперь зададим базовую ссылку на наше API:

# базовый URL для API
const BASE = "http://localhost:5000"

И объявим тип заметки для дальнейшей обработки:

# тип для заметки
type Note = object
  id: cint
  name: cstring
  completed: bool

После чего объявим реактивные переменные:

var
  # реактивный список заметок
  notes = remember newSeq[Note]()
  # реактивное название для новой заметки
  newName = remember ""

Теперь вернемся к серверной части и вспомним, что мы там писали. Создадим три процедуры для взаимодействия с API:

proc updateNotes() {.async.} =
  # Делаем запрос к серверу на получение всех заметок
  await fetch(fmt"{BASE}/notes".cstring)
    # Получаем JSON
    .then((response: Response) => response.json())
    .then(proc(data: JsObject) =
      # Преобразуем JSON в список Note
      var tmpNotes: seq[Note] = collect:
        for i in data["response"]["items"]:
          i.to(Note)
      # Если размер списка не изменился - просто меняем параметры
      if notes.len == tmpNotes.len:
        for i in 0..<tmpNotes.len:
          notes[i] = tmpNotes[i]
      else:
        # Если размер списка изменился - полностью меняем список
        notes.set(tmpNotes)
    )


proc toggleNote(note: Note) {.async.} =
  # Отправляем PATCH запрос
  discard await fetch(fmt"{BASE}/note/{note.id}".cstring, newfetchOptions(
    HttpPatch, $(%*{"completed": not note.completed})
  ))


proc addNote(name: string) {.async.} =
  # Отправляем POST запрос
  discard await fetch(fmt"{BASE}/note".cstring, newfetchOptions(
    HttpPost, $(%*{"name": name})
  ))

Давайте получим список заметок при загрузке страницы:

# Сразу получаем список заметок
discard updateNotes()

А теперь время верстки!

# Объявляем наше одностраничное приложение в элементе с ID app
appRoutes "app":
  # Главный маршрут
  "/":
    tDiv(class = "flex flex-col gap-2 w-fit p-8"):
      tDiv(class = "flex"):
        # input для 
        tInput(id = "newNameChanger", class = "rounded-l-full px-6 py-2 border-2 outline-none", value = $newName):
          @input:
            # Меняем название заметки
            newName.set($ev.target.InputElement.value)
        tButton(class = "bg-green-400 hover:bg-green-500 active:bg-green-600 rounded-r-full px-4 py-2 transition-all duration-300"):
          "Добавить"
          @click:
            # Добавляем новую заметку
            discard await addNote(newName)
            discard await updateNotes()
            newName.set("")
      tDiv(class = "flex flex-col gap-2"):
        # Пробегаемся по заметкам
        for i in 0..<notes.len:
          tDiv(
            class =
              # Меняем класс в зависимости от выполненности заметки
              if notes[i].completed:
                "rounded-full select-none px-6 py-2 cursor-pointer hover:scale-110 translation-all duration-300 bg-green-300"
              else:
                "rounded-full select-none px-6 py-2 cursor-pointer hover:scale-110 translation-all duration-300 bg-red-300"
          ):
            # аналогично с эмоджи
            if notes[i].completed:
              "✅ "
            else:
              "❌ "
            {notes[i].name}
            @click:
              # При нажатии шлем PATCH запрос и обновляем список заметок
              discard await toggleNote(notes[i])
              discard await updateNotes()

Наконец-то, теперь мы можем запустить?

Все верно! Самое время протестировать наше приложение! Запускаем бэкенд:

nim c -r server/src/main

И отдельно запускаем фронтенд:

cd client
hpx dev --reload --port 8000
Итоговое приложение с запущенным бэкендом и фронтендом
Итоговое приложение с запущенным бэкендом и фронтендом

Вот и все! Мы написали простое веб-приложение, используя Nim и веб-фреймворк HappyX.

Полезные ссылки

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


  1. yarkov
    29.06.2024 10:14
    +3

    Жесть какая ))


  1. Sollita
    29.06.2024 10:14
    +1

    Ничего, что первая картинка не соответствует Вашему коду?


    1. akihayase Автор
      29.06.2024 10:14

      Ничего страшного в этом нет, это же всего лишь обложка статьи


      1. Sollita
        29.06.2024 10:14

        А, по-моему, это введение читателей в заблуждение. Если бы не эта картинка, я бы вообще не стала читать эту статью.

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

        Я знаю, как сделать то, что на первой картинке нарисовано, а Вы?


        1. blackmius
          29.06.2024 10:14

          тёть, не душни