Всем доброго дня! В предыдущей статье Kawai-Focus 2.2: Python-бинарник в Tauri — проблемы и альтернативы:
Освещены неработающие моменты с бинарником на Arch Linux;
Рассмотрены альтернативы, которые могут исправить проблемы с бинарником;
Внедрён оптимальный (для меня) вариант, который исправил половину неисправностей.
В данной статье я покажу код на JS, который не поместился в предыдущей статье, а также перепишу его на TS. Кратко расскажу о преимуществах TS над JS и о том, что необходимо понимать для перехода.
В прошлой статье я также упоминал, что у Сергея получилось запустить мой проект на Tauri в режиме разработки на Arch. Он поделился со мной информацией в issue на GitHub и тем самым внёс вклад в проект. Поэтому я решил попробовать исправить проблему на основе его issue. Заодно расскажу, что такое issue и как оно выглядит.
Заваривайте чай, доставайте вкусняшки — пора «снимать первый урожай помидор»! 🍅
Логика приложения на TS
Как я писал в предыдущей статье, логика, которую я реализовал на JS, не поместилась в неё полностью, поэтому я решил подробнее рассказать об этом коде здесь. Ранее я уже имел дело с чистым JS, хоть и не очень много, поэтому написание прототипа не оказалось для меня сложной задачей.
Однако чем больше кода я писал, тем сильнее осознавал, что чистый JS подходит в основном для относительно простых задач. Например, в нём отсутствует строгая типизация и ряд возможностей, к которым я привык при работе с Python. В прошлой статье я использовал JS, чтобы ускорить разработку, поскольку параллельно было много других задач. Освоение TS тогда заняло бы слишком много времени — для меня он был «тёмным лесом».
К счастью, сейчас у меня появилось больше времени TS, чтобы улучшить текущую реализацию и при этом переписав минимальное количество кода.
Почему минимум?
Потому что TypeScript — это надстройка над JavaScript. Он не заменяет JS, а расширяет его. Большая часть уже написанного кода остаётся валидной и продолжает работать без изменений. В большинстве случаев переход начинается с простого добавления типов к переменным, функциям и возвращаемым значениям.
Кроме того, TypeScript компилируется в обычный JavaScript, поэтому логика приложения остаётся прежней — меняется лишь уровень контроля на этапе разработки. Фактически, разработчик не переписывает архитектуру, а постепенно усиливает её типами. Это особенно удобно, когда проект уже рабочий: можно внедрять TS поэтапно, начиная с отдельных файлов.
Главное преимущество TS перед JS — возможность находить ошибки ещё до запуска программы, улучшенная читаемость и поддерживаемость кода, а также более удобная работа в больших проектах благодаря автодополнению и строгой структуре типов.
Установка необходимых плагинов
Первым делом мне нужно установить необходимые для работы c Tauri через JS/TS плагины, а лишние, которые ещё остались со времен Python бинарника удалить.
Перехожу в папку client:
cd client
Для начала удалю @tauri-apps/plugin-shell , который позволяет вызывать системные команды и запускать внешние процессы (например, Python-бинарник) из приложения, управляя ими через безопасный интерфейс между фронтендом и нативной частью.
npm uninstall @tauri-apps/plugin-shell
Теперь установлю пару плагинов, которые мне пригодятся для работы.
npm install @tauri-apps/plugin-sql @tauri-apps/api
Разбор новых плагинов:
@tauri-apps/plugin-sql— плагин Tauri, который добавляет в приложение доступ к SQL‑базам (например, SQLite) через единый API для выполнения запросов и работы с по��ключениями;@tauri-apps/api— основной клиентский пакет JS/TS с API Tauri для взаимодействия фронтенда с “нативной” частью (окна, файловая система, диалоги, события, invoke к Rust-командам и т.п.).
Работа с базой данных
Первым делом я написал скрипт, который будет создавать базу данных при старте приложения, если он не обнаружит её в указанном каталоге. Логика должна быть максимально разделена, чтобы при необходимости можно было заменить CRUD-подход с JS/TS на подход через API для веб-версии, изменив при этом минимум кода.
Плагин tauri-plugin-sql работает с базой данных не через ORM-подход, а через SQL-запросы, которые программист вставляет в нужные функции в виде строк. Если база данных небольшая и состоит из одной таблицы timer, как в моём случае, то SQL-подход вполне подойдёт. Кроме того плагин довольно лёгкий, что даёт плюс по ресурсам когда проект небольшой.
Когда-то давно мне доводилось писать прототип базы данных MySQL на чистых DML- и DDL-запросах. Кроме того, в некоторых компаниях этот навык требуется, поэтому его использование будет полезно для освежения знаний SQL.
DML (Data Manipulation Language) — это группа SQL-операторов для работы с данными в таблицах: выборка, вставка, обновление и удаление (например, SELECT, INSERT, UPDATE, DELETE).
DDL (Data Definition Language) — это группа SQL-операторов для описания и изменения структуры объектов базы данных (например, CREATE, ALTER, DROP).
timerDDL.js
Сначала я создаю файл client/db/ddl/timerDDL.js для формирования SQL-запроса на создание базы данных. Поскольку база данных у меня уже была создана ранее, я могу посмотреть схему таблицы timer в виде DDL-запроса в VS Code, открыв её через плагин SQLite3 Editor.

timerDDL.js
export const CREATE_TIMER = ` PRAGMA foreign_keys = ON; CREATE TABLE IF NOT EXISTS timer ( title VARCHAR(200) NOT NULL, pomodoro_time INTEGER NOT NULL, break_time INTEGER NOT NULL, break_long_time INTEGER NOT NULL, count_pomodoro INTEGER NOT NULL, id INTEGER NOT NULL, PRIMARY KEY (id) ); `;
Разбор кода:
export const CREATE_TIMER— экспортируемая JavaScript константа для SQL-запроса;Обратные кавычки (backticks) — шаблонная строка (template literal) для многострочного SQL;
PRAGMA foreign_keys = ON;— включает поддержку внешних ключей в SQLite (иначе они игнорируются);CREATE TABLE IF NOT EXISTS— создаёт таблицуtimer, только если её ещё нет (безопасно при повторном выполнении);title VARCHAR(200) NOT NULL— название таймера, до 200 символов, обязательно;pomodoro_time INTEGER NOT NULL— длительность одного помидоро в минутах;break_time INTEGER NOT NULL— длительность короткого перерыва;break_long_time INTEGER NOT NULL— длительность длинного перерыва;count_pomodoro INTEGER NOT NULL— количество помидоро в цикле до длинного перерыва;id INTEGER NOT NULL— уникальный идентификатор таймера;PRIMARY KEY (id)—idявляется первичным ключом (уникальный, индексированный);Точка с запятой в конце — корректное завершение SQL-скрипта.
Данный .js-файл хранит простую переменную со строкой, поэтому для преобразования в TypeScript достаточно переименовать его расширение с .js на .ts.
timerDML.js
Далее я создал client/src/db/dml/timerDML.js для работы с таблицей timer.
export const SELECT_TIMERS = 'SELECT id, title, pomodoro_time, count_pomodoro FROM timer ORDER BY id DESC' export const COUNT_TIMERS = 'SELECT COUNT(*) as cnt FROM timer' export const INSERT_SEED_DB = ` INSERT INTO timer (title, pomodoro_time, break_time, break_long_time, count_pomodoro) VALUES ('Timer mini example', 10, 3, 15, 2), ('Timer max example', 90, 10, 40, 8) `
SELECT_TIMERS:
SELECT id, title, pomodoro_time, count_pomodoro— выбирает ключевые поля для отображения;FROM timer— из таблицы таймеров;ORDER BY id DESC— сортировка по ID по убыванию (новые таймеры сверху).
COUNT_TIMERS:
SELECT COUNT(*)— подсчёт общего количества записей в таблице;as cnt— псевдоним результата ({ cnt: 5 });Используется для пагинации или проверки пустоты БД.
INSERT_SEED_DB:
INSERT INTO timer (...)— вставка начальных данных (seed);Множественная вставка — 2 записи за один запрос (эффективнее);
('Timer mini example', 10, 3, 15, 2)— короткий таймер: 10 мин работа, 3 мин перерыв, 15 мин длинный, 2 помидоро на цикл;('Timer max example', 90, 10, 40, 8)— длинный таймер: 90 мин работа, 10 мин перерыв, 40 мин длинный, 8 помидоро на цикл.idне указан — автоинкремент (PRIMARY KEY).
Здесь я, аналогично предыдущему файлу, переименовал расширение с .js на .ts.
config.js
После того как я создал SQL-запросы в виде строк, я приступил к реализации механизма, который будет их использовать. Чтобы добраться до самих запросов, необходимо сначала подключиться к базе данных, для чего нужно сформировать url. Для этого я создал файл client/src/config.js, в котором реализовал функцию getDB_URL().
import { appLocalDataDir } from '@tauri-apps/api/path'; export async function getDB_URL() { const appDir = await appLocalDataDir(); return `sqlite:${appDir}/timer.db`; }
Разбор кода:
import { appLocalDataDir }— импортирует API Tauri для получения локальной папки приложения;export async function getDB_URL()— асинхронная функция, во��вращающая URL базы данных;await appLocalDataDir()— получает путь к папке данных приложения:Windows:
C:\Users<user>\AppData\Local<appname>;macOS:
~/Library/Application Support/<appname>;Linux:
~/.local/share/<appname>.
sqlite:${appDir}/timer.db— формирует URI для SQLite-плагина Tauri:sqlite:— схема протокола для@tauri-apps/plugin-sql;${appDir}/timer.db— путь к файлуtimer.dbв папке приложения.
Результат:
"sqlite:/home/user/.local/share/kawai-focus/timer.db"
В данной функции не хватает аннотации возвращаемого значения и строки документации.
import { appLocalDataDir } from '@tauri-apps/api/path'; /** Возвращает URL подключения к SQLite */ export async function getDB_URL(): Promise<string> { const appDir = await appLocalDataDir(); return `sqlite:${appDir}/timer.db`; }
Promise<string> в TypeScript — это тип, который означает, что функция возвращает промис — объект, представляющий результат асинхронной операции.
Промис может находиться в трёх состояниях:
ожидание (pending);
выполнен успешно (fulfilled /
resolve);завершён с ошибкой (rejected /
reject).
Если промис завершается успешно, то в случае Promise<string> он вернёт значение типа string. Это даёт статическую проверку типов и гарантирует, что результат асинхронной операции будет именно строкой, а не числом, объектом или undefined.
seed.ts
Когда запускается приложение Kawai-Focus, оно должно заполнить базу демонстрационными данными. В моём случае — двумя демонстрационными таймерами в таблице timer. Это должно происходить только в том случае, если база данных пуста. Для этого я создал файл client/src/db/seed.ts, в котором будет находиться функция seedDb() для заполнения данных.
Думаю, с .js-файлами я уже привёл достаточно примеров, а основное отличие заключается в использовании типов в TS. Поэтому далее я буду сразу показывать .ts-файлы.
Перед тем как перейти к коду функции seedDb(), я создам для неё тип, который поможет с валидацией. В данном случае мне потребуется валидация количества записей в таблице. Для этого я создам файл client/src/types/timerType.ts.
timerType.ts
export type CountRow = { cnt: number; };
Разбор кода:
export— делает тип доступным для использования в других файлах проекта;type— объявляет пользовательский тип в TypeScript;CountRow— имя типа, которое описывает структуру строки результата SQL-запроса;{ cnt: number; }— объектный тип с одним свойством:cnt— поле, соответствующее алиасу в SQL-запросе (например,SELECT COUNT(*) as cnt);number— тип значения (число);
Назначение типа — гарантирует, что результат запроса
COUNT_TIMERSбудет содержать полеcntименно числового типа, что позволяет избежать ошибок при обращении к нему;
seed.ts
import Database from '@tauri-apps/plugin-sql'; import { INSERT_SEED_DB, COUNT_TIMERS } from './dml/timerDML'; import { CountRow } from '../types/timerType'; /** Заполняет бд данными (демо таймерами) */ export async function seedDb(db: Database) { const count = await db.select<CountRow[]>(COUNT_TIMERS); const cnt = count[0]?.cnt ?? 0; if (cnt = 0) { await db.execute(INSERT_SEED_DB); } }
Разбор кода:
import Database from '@tauri-apps/plugin-sql'— импортирует классDatabaseиз плагина Tauri для работы с SQL-базой данных (SQLite, MySQL и др. в зависимости от конфигурации);import { INSERT_SEED_DB, COUNT_TIMERS }— импортирует SQL-запросы:COUNT_TIMERS— запрос для получения количества записей в таблицеtimer;INSERT_SEED_DB— запрос для вставки демонстрационных таймеров;
import { CountRow }— импортирует TypeScript-тип, описывающий структуру строки результата запроса подсчёта (например,{ cnt: number });export async function seedDb(db: Database)— асинхронная функция, которая принимает подключение к базе данных и выполняет начальное заполнение таблицы;awaitdb.select<CountRow[]>(COUNT_TIMERS)— выполняет SQL-запрос на выборку:<CountRow[]>— указывает тип возвращаемых данных (массив строк результата);результатом будет массив объектов, соответствующих структуре
CountRow;
const cnt = count[0]?.cnt ?? 0;:count[0]?.cnt— безопасно получает значение поляcntиз первой строки результата;?.— optional chaining (защита отundefined);?? 0— если значение отсутствует, используется0по умолчанию;
if (cnt = 0)— проверяет, пуста ли таблицаtimer;await db.execute(INSERT_SEED_DB);— если таблица пуста, выполняет SQL-запрос вставки демо-данных;Результат: при первом запуске приложение автоматически заполняет базу демонстрационными таймерами, а при повторных запусках данные не дублируются.
tsconfig.json
Вы, наверное, уже обратили внимание на неудобные относительные пути в импортах вроде './dml/timerDML', которые режут глаза и сильно неудобны. Для упрощения написания импортов можно указать в client/src/tsconfig.json, чтобы корень приложения ассоциировался с @/. Таким образом не нужно будет прыгать по относительным путям туда-сюда.
{ "compilerOptions": { "baseUrl": ".", "paths": { "@/*": ["src/*"] }, "module": "ESNext", "target": "ES2020", "moduleResolution": "Bundler" } }
Разбор конфига:
baseUrl: "."— задаёт базовую директорию проекта для TypeScript, от которой будут считаться пути;paths— позволяет создать алиасы для импортов:"@/*": ["src/*"]—@/теперь соответствует папкеsrc, что сокращает и упрощает импорты;
module: "ESNext"— указывает формат модулей (поддержка современных ES-модулей);target: "ES2020"— определяет версию JavaScript, в которую будет транслироваться код;moduleResolution: "Bundler"— указывает, как TypeScript будет разрешать пути модулей, оптимально для сборщиков вроде Vite или Webpack.
Старые импорты в seed.ts:
import { INSERT_SEED_DB, COUNT_TIMERS } from './dml/timerDML'; import { CountRow } from '../types/timerType'
Новые импорты в seed.ts:
import { INSERT_SEED_DB, COUNT_TIMERS } from '@/db/dml/timerDML' import { CountRow } from '@/types/timerType'
Теперь импорты стали намного удобнее и читаемее.
initDb.ts
Далее мне понадобилась функция getDb(), которая инициализирует базу данных и возвращает подключение к ней. Для этого я создал файл client/src/db/initDb.ts.
import Database from '@tauri-apps/plugin-sql'; import { CREATE_TIMER } from '@/db/ddl/timerDDL'; import { getDB_URL } from '@/config'; import { seedDb } from '@/db/seed'; let dbPromise: Promise<Database> | null = null; /** Получает подключение к бд */ export async function getDb(): Promise<Database> { if (!dbPromise) { dbPromise = (async () => { try { const dbUrl = await getDB_URL(); const db = await Database.load(dbUrl); await db.execute(CREATE_TIMER); await seedDb(db); return db; } catch (error) { console.error('Ошибка инициализации базы данных', error); throw error; } })(); } return await dbPromise; }
Функция getDb()
Асинхронная функция, которая возвращает подключение к базе данных (
Database);Использует паттерн singleton: создаётся один экземпляр подключения и повторно используется при последующих вызовах;
Основные шаги при первом вызове:
Получает URL базы данных через
getDB_URL();Загружает базу данных с помощью
Database.load(dbUrl);Создаёт таблицу
timer, если она ещё не существует (CREATE_TIMER);Заполняет таблицу демонстрационными данными через
seedDb(db).
В случае ошибки логирует её в консоль и пробрасывает дальше;
При повторных вызовах возвращает уже готовое подключение, не создавая новый экземпляр.
timerCrud.ts
Наконец я дошёл до первой CRUD-операции, которая является частью функционала приложения. Для этого нужно реализовать select-операцию, которая получает список таймеров. CRUD-операции для таймеров будут находиться в client/src/db/crud/timerCrud.ts.
Для проверки данных, которые функция будет получать из базы данных, я создал тип TimersRow.
timerType.ts
export type TimersRow = { id: number; title: string; pomodoro_time: number; count_pomodoro: number; };
timerCrud.ts
import { getDb } from "@/db/initDb"; import { SELECT_TIMERS } from "@/db/dml/timerDML"; import { TimersRow } from "@/types/timerType" /** Получает список таймеров */ export async function getTimers(): Promise<TimersRow[]> { const db = await getDb(); return await db.select<TimersRow[]>(SELECT_TIMERS); }
Функции getTimers():
Асинхронная функция, которая возвращает список всех таймеров из базы данных;
Подключается к базе через
getDb();Выполняет SQL-запрос
SELECT_TIMERSи возвращает результат в виде массива объектов типаTimersRow;Возвращаемый тип:
Promise<TimersRow[]>, что гарантирует корректность данных через TypeScript.
Этого достаточно, чтобы начать работать с базой данных. Единственное, чего я пока не реализовал, — это работа с миграциями. Хорошая новость в том, что tauri-plugin-sql их поддерживает. Я обязательно внедрю в этот проект миграции, которые позволят удобно расширять базу данных по мере необходимости, но сделаю это в одной из следующих статей.
Экран Таймеры
Следующее и очень важное, что я должен был реализовать, — это экран «Таймеры». В данном случае это веб-страница со стилями и логикой на TypeScript, которая вызывает CRUD-функцию, получает данные таймеров и отображает их.
Данная статья не о HTML и CSS, поэтому я не буду на них останавливаться, а уделю внимание логике на TypeScript и Vue.
TimersList.ts
Я написал файл client/src/views/TimersList.ts, который необходим для логики работы экрана Таймеры.
import { defineComponent, onMounted, ref } from 'vue' import { IonIcon } from '@ionic/vue' import { timerOutline, chevronUp, chevronDown, playCircleOutline, pencilOutline, trashOutline, } from 'ionicons/icons' import { getTimers } from '@/db/crud/timerCrud' import type { TimersRow } from '@/types/timerType' export default defineComponent({ name: 'TimersList', components: { IonIcon }, setup() { const timers = ref<TimersRow[]>([]) const loading = ref<boolean>(true) const error = ref<string | null>(null) const expandedId = ref<number | null>(null) const loadTimers = async (): Promise<void> => { loading.value = true error.value = null try { const result = await getTimers() timers.value = result } catch (e: unknown) { error.value = e instanceof Error ? e.message : 'Ошибка загрузки таймеров' } finally { loading.value = false } } const toggleExpand = (id: number): void => { expandedId.value = expandedId.value = id ? null : id } onMounted(loadTimers) return { timers, loading, error, expandedId, toggleExpand, timerOutline, chevronUp, chevronDown, playCircleOutline, pencilOutline, trashOutline, } }, })
Разбор TimersList.vue:
Это Vue 3 компонент, написанный с использованием Composition API.
Импортирует иконки из Ionicons (
IonIcon) для отображения интерфейса.Получает данные из базы через функцию
getTimers()изtimerCrud.
Основные части:
Состояния (
ref):timers— массив таймеров (TimersRow[]);loading— индикатор загрузки;error— текст ошибки (если загрузка не удалась);expandedId— id таймера, который в данный момент раскрыт (для UI).
Функции:
loadTimers()— асинхронно загружает таймеры из базы, обрабатывает ошибки и обновляет состояние загрузки;toggleExpand(id)— переключает раскрытие/сворачивание таймера в списке.
Хук жизненного цикла:
onMounted(loadTimers)— при монтировании компонента автоматически запускает загрузку таймеров.
Возврат значений:
Все состояния и функции возвращаются из
setup()для использования в шаблоне;Иконки экспортируются для удобного использования в интерфейсе.
Суть: компонент отображает список таймеров из базы данных, поддерживает индикатор загрузки, обработку ошибок и возможность раскрытия деталей конкретного таймера.
index.ts
Следующее, что я реализовал в проекте — маршрутизацию. Она нужна, чтобы переходить между страницами приложения и корректно отображать компоненты Vue.
import { createRouter, createWebHistory } from '@ionic/vue-router' import type { RouteRecordRaw } from 'vue-router' import TimersList from '@/views/TimersList/TimersList.vue' const routes: RouteRecordRaw[] = [ { path: '/', redirect: '/timers', }, { path: '/timers', name: 'Timers', component: TimersList, }, ] const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes, }) export default router
Разбор index.ts:
Импортирует функции
createRouterиcreateWebHistoryиз@ionic/vue-routerдля настройки маршрутизатора;Определяет массив маршрутов
routesтипаRouteRecordRaw[]:/— перенаправляет на/timers;/timers— отображает компонентTimersList.
Создаёт роутер
routerс историей браузера (createWebHistory) и подключёнными маршрутами;Экспортирует роутер для использования в основном приложении.
Суть: этот файл отвечает за навигацию внутри приложения, позволяя Vue корректно отображать нужные страницы при переходах.
vite-env.d.ts
Мне нужен небольшой, но важный файл, который помогает TypeScript понимать типы, связанные с Vite. Без него редактор ругался на глобальные переменные и импорты из Vite.
/// <reference types="vite/client" />
Строка /// <reference types="vite/client" /> подключает глобальные типы Vite.
TimersList.vue
Теперь я покажу, как устроен сам компонент Vue для экрана «Таймеры». В этом файле подключаются шаблон, логика на TypeScript и стили, чтобы всё было красиво и работало вместе.
<!-- TimersList.vue --> <template src="@/views/TimersList/TimersList.html"></template> <script lang="ts" src="@/views/TimersList/TimersList.ts"></script> <style src="@/views/TimersList/TimersList.css" scoped></style>
Разбор TimersList.vue:
<template>— подключает HTML-шаблон компонента из отдельного файлаTimersList.html;<script lang="ts">— подключает TypeScript-логику изTimersList.ts, где описан функционал компонента;<style scoped>— подключает CSS-стили изTimersList.cssи ограничивает их действие только этим компонентом.
Суть: этот файл объединяет шаблон, логику и стили компонента, сохраняя код чистым и модульным.
App.vue
App.vue — это корневой компонент приложения. Он задаёт базовую структуру и подключает роутер, чтобы страницы отображались корректно.
<template> <ion-app> <ion-router-outlet /> </ion-app> </template> <script setup> import { IonApp, IonRouterOutlet } from '@ionic/vue' </script>
Разбор App.vue:
<template>— оборачивает всё приложение в компонентIonAppиз Ionic;<ion-router-outlet />— контейнер для отображения страниц в зависимости от маршрута;<script setup>— импортирует необходимые компоненты Ionic (IonApp,IonRouterOutlet) и подключает их к шаблону.
Суть: этот файл создаёт основу приложения и обеспечивает место для динамического отображения страниц через роутер.
main.ts
Настало время для «финального аккорда» — файла main.ts, который собирает все компоненты в приложение.
import { createApp } from 'vue' import App from '@/App.vue' import router from '@/router' import { IonicVue } from '@ionic/vue' /* Core CSS required for Ionic components to work properly */ import '@ionic/vue/css/core.css' /* Basic CSS for apps built with Ionic */ import '@ionic/vue/css/normalize.css' import '@ionic/vue/css/structure.css' import '@ionic/vue/css/typography.css' /* Optional CSS utils */ import '@ionic/vue/css/padding.css' import '@ionic/vue/css/float-elements.css' import '@ionic/vue/css/text-alignment.css' import '@ionic/vue/css/text-transformation.css' import '@ionic/vue/css/flex-utils.css' import '@ionic/vue/css/display.css' /* Theme variables */ import '@/theme/variables.css' const app = createApp(App) .use(IonicVue) .use(router) router.isReady().then(() => { app.mount('#app') })
Разбор main.ts:
Импорт Vue и корневого компонента:
createAppиApp.vue;Импорт роутера: подключает маршрутизацию;
Использование IonicVue: интегрирует компоненты и стили Ionic.
Подключение CSS:
Core CSS и базовые стили для корректной работы компонентов;
Опциональные утилиты для отступов, выравнивания текста, flex и display;
Темы и переменные проекта (
variables.css).
Создание приложения:
createApp(App).use(IonicVue).use(router).Монтирование приложения: ждёт готовности роутера (
router.isReady()) и монтирует в элемент сid="app".
Суть: этот файл собирает приложение из компонентов, подключает роутер и стили Ionic и запускает его в браузере.
В конце нужно обязательно поменять .js на .ts в файле index.html.
<script type="module" src="/src/main.ts"></script>
На этом написание кода для экрана «Таймеры» завершено.
Исправление проблемы c Arch используя issue
В конце прошлой статьи я рассказывал, что Сергей, который помогал мне тестировать приложение на Arch Linux, создал для меня issue в GitHub.
Issue — это запись или сообщение в репозитории, с помощью которого можно описать проблему, задачу или предложение по улучшению проекта. В нём обычно указывают:
что произошло (описание ошибки или задачи);
шаги для воспроизведения проблемы;
ожидаемый результат;
фактический результат;
дополнительные файлы, скриншоты или логи.
Суть: issue помогает отслеживать баги, запросы на улучшения и организовывать работу над проектом в команде.

Текст issue (перевод с english)
Описание ошибки: Предоставленный AppImage не запускается в Arch Linux (протестировано на Hyprland/BSPWM). В терминале отображается следующая ошибка: Не удалось создать EGL-дисплей по умолчанию: EGL_BAD_PARAMETER
Основная причина: Это известная проблема с webkit2gtk в дистрибутивах с непрерывным обновлением (например, Arch) при работе в Wayland или некоторых средах X11. Аппаратное ускорение/режим композитинга в WebKit конфликтует с инициализацией EGL-дисплея системы.
Решение (протестировано): Мне удалось успешно собрать проект из исходного кода на моей машине Arch с определенным исправлением. Добавление простого переключения переменной окружения в main.rs перед запуском сборщика Tauri полностью решает проблему.
Предложенное изменение кода: В файле desktop/src-tauri/src/main.rs измените функцию main следующим образом:
fn main() { // Fix for EGL_BAD_PARAMETER on Arch Linux #[cfg(target_os = "linux")] { std::env::set_var("WEBKIT_DISABLE_COMPOSITING_MODE", "1"); } println!("Starting Tauri application..."); app_lib::run(); }
Локальная сборка: После применения этого изменения я запустил команду cargo tauri dev, и приложение запустилось без проблем.
Бинарный файл: Скомпилированный релизный бинарный файл также работает без каких-либо внешних флагов или переменных окружения.
Дополнительные замечания: Текущая структура проекта несколько сложна (разделена на папки клиента и рабочего стола). Это может вызывать проблемы для автоматических сборщиков или GitHub Actions при создании AppImage, что приводит к отсутствию ресурсов или некорректным путям. Упрощение структуры или обеспечение корректного указания файла tauri.conf.json на ресурсы фронтенда повысит надежность сборки.
Пожалуйста, рассмотрите возможность добавления этого исправления в основную ветку для поддержки пользователей Linux на современных графических процессорах.
Исправление бага
В issue написано подробно как устранить проблему, но решение слишком радикальное чтоб делать WEBKIT_DISABLE_COMPOSITING_MODE в 1 у всех на Linux.
WEBKIT_DISABLE_COMPOSITING_MODE — это переменная окружения WebKitGTK, которая принудительно отключает accelerated compositing (аппаратно-ускоренный композитинг/рендеринг) в WebKit, и её нельзя выключать всем пользователям, потому что это глобально переводит WebView на более “safe”, но потенциально более медленный/менее плавный режим и может ухудшить производительность/поведение там, где проблемы с EGL вообще нет.
Вместо радикального решения я добавил флаг --disable-webkit-compositing, которым можно отключить accelerated compositing если возникла ошибка Could not create default EGL display: EGL_BAD_PARAMETER.
И это, к сожалению, не исправило его проблему с запуском AppImage. Одна ошибка сменяла другую. Например, появилась ошибка sqfs_read_range error, которая означает, что при чтении файловой системы SquashFS внутри AppImage произошёл сбой.

Это стало для меня последней каплей. Мало того, что AppImage включает в себя все зависимости и весит 94 МБ (для сравнения, версия deb занимает всего 4,5 МБ), так ещё и в некоторых случаях чтение его файловой системы вызывает проблемы. Поэтому я решил поступить радикально и полностью от него отказаться.
Для Arch Linux есть более правильный вариант, о котором я расскажу чуть позже, а пока я уберу AppImage из проекта.
Фрагмент файла tauri.conf.json:
"targets": ["deb", "rpm"],
Я просто указал в списке "targets" форматы, которые мне нужны, а неуказанные он собирать не будет.
Анонс на следующие статьи
Сегодня я наконец доделал основу приложения Kawai-Focus-v2, которая позволит мне легко расширить проект до того вида, который у меня был на Kivy.
Однако у меня остаётся незакрытый «генштальт» с Arch Linux. Самое правильное для Arch, и это то, о чём меня просили изначально арчеводы, — это доб��вить описание сборки в AUR.
AUR в Arch — это Arch User Repository: пользовательский репозиторий, поддерживаемый сообществом. Он содержит не готовые бинарные пакеты, как официальные репозитории, а в основном описания сборки (PKGBUILD), по которым собирается пакет (обычно с помощью makepkg) и потом устанавливается через pacman.
В следующей статье для тестирования и более глубокого понимания операционной системы я установлю полноценный Arch Linux на свой ПК. Также попробую на нём собрать и запустить своё приложение и в конце добавить его в AUR.
Я сделал ещё кое-какие настройки для deb пакета, о которых расскажу в следующей статье, так как этот материал не поместился в текущую. Ещё хочу похвастаться, что я выложил предварительные релизы deb и rpm на GitHub. Если кто-то захочет их протестировать на своих системах, пишите о результатах в комментариях.
Если у вас есть мысли о том, как можно улучшить проект, пишите в комментариях — с удовольствием ознакомлюсь с вашими предложениями!
Читайте продолжение — не пропустите!
Заключение
Переписана логика с JS на TS;
Исправлена вторая проблема запуска на Arch по issue Сергея (пришлось отключить сборку AppImage).
Ссылки к статье
Репозиторий проекта на Github Kawai-Focus-v2.
