К старту курса по Fullstack-разработке на Python делимся материалом о том, как при помощи современных возможностей CSS и JS — ворклетов и API Houdini — подключиться к базе данных и выполнять запросы к ней. За подробностями приглашаем под кат.


Как это свойственно твитам, на той неделе в сети витал этот твит:

Рекрутеры: «Мы ищем того, кто может подключиться к базе данных с помощью CSS»
Рекрутеры: «Мы ищем того, кто может подключиться к базе данных с помощью CSS»

Давненько мне не попадалось достойных щитпостинговых проектов — ещё с тех времён, когда я даже не знал слова «щитпост». Потому отчасти меня вдохновил предыдущий проект на основе блокчейн-стартапа, когда его инвесторы захотели запечатлеть свои лица на 3D-кубах.

Это напоминает о старых добрых временах на заре интернета, когда всё было в диковинку. Но я не стремлюсь к предписаниям, поэтому избавлю вас от истории моей жизни. Лучше расскажу о том, как я справился со своим новым проектом — sqlcss.xyz:

Он о том, как подключаться к БД из CSS. Работает только в Chrome, зато можно сделать запрос в любую БД SQLite через CSS. Как именно? Набор новых API Houdini даёт браузеру управлять CSS с помощью JavaScript и объектной модели браузера: создавать ваши стили CSS, добавлять ваши свойства и т. д.

Возможно, самая важная функциональность Houdini — это CSS Paint Worklet. Она позволяет «рисовать» на элементе, как в Canvas, работая с ним в браузере, как с изображением в CSS. Поиграть с примерами можно на houdini.how. Но этот ворклет — лишь часть API воркера, сам контекст сanvas тоже сильно урезан и песочница для рисования в CSS-коде меньше, чем можно ожидать.

Это значит, что нет доступа к сети Можно распрощаться с fetch и XmlHttpRequest. А ещё нет drawText в контексте рисования. На всякий случай, если вы надеялись обойти эти проблемы — другие API JS тоже исчезли. Но не всё потеряно. Разберём процесс поэтапно.

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 для импорта и просмотра локальной БД. Но как красиво отображать результаты, когда их много и всё нужно разделить на отдельные строки? Это уже другая тема, но если вы хотите разобраться в ней, вот весь код проекта.

Продолжить погружение в CSS и другие веб-технологии вы сможете на наших курсах:

Узнайте подробности здесь.

Профессии и курсы

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


  1. muxa_ru
    09.02.2022 00:12
    +15

    с помощью JavaScript


  1. Ul3ainee
    09.02.2022 05:28
    +5

    По идее, CSS3 тьюринг-полный (хотя дискуссии идут), так что чисто теоретически можно хоть весь сетевой стек на нём написать (как хорошо, что я не употребляю подобных препаратов).


    1. codecity
      09.02.2022 11:35
      +5

      Тьюринг-полный - это значит, что можно реализовать любой алгоритм. Но вот про доступ к системе - это не входит в понятие полноты и Тьюрингу и вообще никак не формализуется (т.е. еще не придумали характеристику - насколько тот или иной язык способен использовать все имеющиеся возможности системы). CSS доступа к сокетам, насколько я знаю, не имеет. Будь он хоть трижды Тьюринг-полный - возможности ограничены доступом к системе.


      1. venanen
        10.02.2022 12:57
        +1

        Доступ к системе ограничен средой выполнения все-таки, а не языком. У JS тоже не особо большой доступ к системе в констексте выполнения браузера. Однако бекенд на нем вполне нормально пишется. Поэтому, если очень сильно захотеть и долго не посещать психиатора - то можно наваять полноценный компилятор css :)


  1. dd84ai
    09.02.2022 06:39

    Тем временем, клиент ожидал скорее всего

    Банального разного рендера HTML/CSS контента в зависимости от того что было получено от запроса из БД.
    Что можно сделать любым бэкендовым фреймворком.

    Просто отображаем полученные данные от БД / Бэкенд АПИшки
    с прямым отображением полученных данных в html/css темплейте.
    (хехе, хорошее место для XSS)


  1. Akuma
    09.02.2022 10:18
    +3

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

    > с помощью JavaScript и объектной модели браузера

    Да, в общем-то, никак, по сути.


  1. nbsp69
    09.02.2022 11:54
    +1

    очень смешно)


  1. dark0ghost
    10.02.2022 17:14

    Ахаха, жду статью как "как написать нейросеть на html" где будет tensorflow.js )