Как подключиться к базе данных с помощью CSS

Kate

Administrator
Команда форума

1. Установка БД​

Она должна быть первым шагом, ведь нужно понять, возможно ли вообще доказательство концепции.

Есть библиотека sql.js. Это буквально версия SQLite, скомпилированная в WebAssembly и через emscripten в старую добрую ASM.js. Воспользоваться версией для WASM нельзя: она должна получать двоичный файл по сети. В версии для ASM этого ограничения нет, и весь код доступен в одном модуле.

Хотя в PaintWorklet доступ к сети внутри воркера ограничен, import кода в модуле ES6 выполнить можно. Иными словами, внутри файла должна быть инструкция export. Но в sql.js нет сборки исключительно для ES6, поэтому скрипт пришлось изменить.

А теперь главный вопрос: можно ли установить БД в ворклете?

const SQL = await initSqlJs({
locateFile: file => `./${file}`,
});

const DB = new SQL.Database();
Получилось! Ошибок нет. Но и данных тоже нет, так что добавим их.

2. Запросы к БД​

Самое простое, что можно сделать вначале — добавить фиктивные данные. В Sql.js есть пара функций именно для этого:

DB.run('CREATE TABLE test (name TEXT NOT NULL)')
DB.run(
'INSERT INTO test VALUES (?), (?), (?), (?)',
['A', 'B', 'C', 'D']
)
Теперь у нас есть тестовая таблица со значениями. Хотя не уверен, как будет структурирован результат. Отправим запрос и получим значения:

const result = DB.exec('SELECT * FROM test')
console.log(result)
Теперь неплохо визуализировать результаты.

3. Простой способ визуализации​

Я думал, это так же просто, как писать текст в сanvas:

class SqlDB {
async paint(ctx, geom, properties) {
const result = DB.exec('SELECT * FROM test');
ctx.font = '32px monospace';
ctx.drawText(JSON.stringify(result), 0, 0, geom.width);
}
}
Но нет: это было бы слишком просто. Контекст здесь не такой, как у элемента в Canvas, то есть осталась только часть контекста. Можно рисовать контуры и кривые, так что отсутствие удобного API — препятствие, но делу оно не помешает.

4. Текст без API для текста​

К счастью, библиотека opentype.js предлагает решение. Она позволяет проанализировать файл шрифта, а затем, получив строку текста, сгенерировать формы букв каждого символа. Итог этой операции — объект path со строкой, которую можно отобразить в контексте рисования.

На этот раз, чтобы импортировать библиотеку opentype.js, вносить изменения не нужно: она уже есть в JSPM. Зададим JSPM npm-пакет, и он автоматически создаст модуль ES6, который можно импортировать прямо в браузер. Это здорово, ведь мне не хотелось возиться с пакетным инструментом ради проекта-шутки.

import opentype from 'https://ga.jspm.io/npm:opentype.js@1.3.4/dist/opentype.module.js'

opentype.load('fonts/firasans.otf')
Здесь проблема, ведь шрифт OpenType нужно загрузить по сети. Я не могу этого сделать. Опять сорвалось! Или могу? Есть метод parse, который принимает буфер массива. Просто кодируем шрифт в base64 и декодируем его в модуле:

import opentype from 'https://ga.jspm.io/npm:opentype.js@1.3.4/dist/opentype.module.js'
import base64 from 'https://ga.jspm.io/npm:base64-js@1.5.1/index.js'

const font = 'T1RUTwAKAIAAAwA ... 3 days later ... wAYABkAGgAbABwAIAKM'

export default opentype.parse(base64.toByteArray(font).buffer)
Я говорил, что в ворклете нет API для обработки строк base64? Нет даже atob и btoa. Пришлось найти реализацию на обычном JS. Этот код я поместил в отдельный файл: не очень это… эргономично — работать с 200 Кб строки кодированного шрифта вместе с оставшейся частью кода.

Вот как, злоупотребив модулем ES, я загрузил шрифт.

5. Отображение результатов, другой простой способ​

Теперь всю тяжёлую работу выполняет библиотека opentype. Всё, что нужно — немного матемологии, чтобы красиво подравнять код:

import font from './font.js'

const SQL = await initSqlJs({
locateFile: file => `./${file}`,
});

const DB = new SQL.Database();

DB.run('CREATE TABLE test (name TEXT NOT NULL)')
DB.run(
'INSERT INTO test VALUES (?), (?), (?), (?)',
['A', 'B', 'C', 'D']
)

class SqlDB {
async paint(ctx, geom, properties) {
const query = DB.exec('SELECT * FROM test')
const result = query[0].values.join(', ')

const size = 48
const width = font.getAdvanceWidth(queryResults, size)
const point = {
x: (geom.width / 2) - (width / 2),
y: geom.height / 2
}

const path = font.getPath(result, point.x, point.y, size)
path.draw(ctx)
}
}

registerPaint('sql-db', SqlDb)
Подправим HTML и CSS и посмотрим, что получится:

<html>
<head>
<script>
CSS.paintWorklet.addModule('./cssdb.js')
</script>
<style>
main {
width: 100vw;
height: 100vh;
background: paint(sql-db);
}
</style>
</head>
<body>
<main></main>
</body>
</html>
Работает, но не хватает CSS, а запрос я захардкодил.

6. Запросы к базе данных через CSS​

Запросы к БД лучше делать с помощью CSS: на самом деле это единственный способ взаимодействовать с воркером рисования вне его контекста — у этого воркера нет API обмена сообщениями, как у обычных воркеров.

Чтобы сделать запрос, потребуется свойство CSS. Определяя inputProperties, подпишемся на изменения нового свойства: при изменении значения свойства оно отобразится повторно. Слушатели событий не нужны:

class SqlDb {
static get inputProperties() {
return [
'--sql-query',
]
}

async paint(ctx, geom, properties) {
// ...
const query = DB.exec(String(properties.get('--sql-query')))
}
}
Это типизированные свойства CSS, но они помещены в класс CSSProperty, который сам по себе не особенно полезен. Чтобы использовать его, как показано выше, придётся вручную преобразовать его в строку, число или что-то ещё. Снова чуть подправим CSS:

main {
// ...
--sql-query: SELECT name FROM test;
}
Кавычки убраны специально, иначе пришлось бы удалять их из строки перед её передачей в БД. Всё работает, миссия выполнена. Если вы уже попробовали sqlcss.xyz, то наверняка заметили, что я на этом не остановился — после небольшого рефакторинга сделал ещё пару изменений.

7. Локальный файл базы данных​

Концепция доказана, но можно добиться большего. Хардкодить схему БД и сами данные — это плохо. Здорово делать запрос в любую БД, когда у вас есть её файл. Нужно просто прочитать его и кодировать в base64, как файл шрифта:

const fileInput = document.getElementById('db-file')
fileInput.onchange = () => {
const reader = new FileReader()
reader.readAsDataURL(fileInput.files[0])

reader.onload = () => {
document.documentElement.style.setProperty(
'--sql-database',
`url('${reader.result}')`
)
}
}
Для этого я создал свойство CSS, где БД SQLite указывается в виде URI из данных в формате base64. URI данных здесь только для подтверждения его валидности с точки зрения DOM — разбор выполним на стороне воркера. Остаётся упростить выполнение запросов, иначе придётся погружаться в отладчик, чтобы работать с CSS элемента.

8. Пишем запросы к БД​

Это, возможно, самая простая часть проекта. Точки с запятой в нашем свойстве CSS — не большая проблема. SQLite до неё нет дела. Если она найдётся во входных данных, проще её удалить:

const queryInput = document.getElementById('db-query')
queryInput.onchange = () => {
let query = queryInput.value;
if (query.endsWith(';')) {
query = query.slice(0, -1)
}

document.documentElement.style.setProperty(
'--sql-query',
queryInput.value
)
}
Теперь можно использовать CSS для импорта и просмотра локальной БД. Но как красиво отображать результаты, когда их много и всё нужно разделить на отдельные строки? Это уже другая тема, но если вы хотите разобраться в ней, вот весь код проекта.


 
Сверху