Вступление
Всем доброго дня! В предыдущей статье 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>.
- Windows:
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)— асинхронная функция, которая принимает подключение к базе данных и выполняет начальное заполнение таблицы;await db.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).
- Получает URL базы данных через
- В случае ошибки логирует её в консоль и пробрасывает дальше;
- При повторных вызовах возвращает уже готовое подключение, не создавая новый экземпляр.
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).
Ссылки к статье
- Мои статьи Arduinum628 на Код на салфетке;
- Репозиторий проекта на Github Kawai-Focus-v2.
Комментарии