使用 CSS 連接數據庫

本人翻譯者系奇舞團前端工程師

原文標題:Yes, I can connect to a DB in CSS

原文作者:Memeloper

原文地址:https://www.leemeichin.com/posts/yes-i-can-connect-to-a-db-in-css.html

故事背景

某公司招聘需求如下:

我們正在尋求可以使用 CSS 連接數據庫的前端夥伴~

自從我上次開始一個高質量的 “發帖” 以來,已經有很長一段時間了,事實上,它已經很長一段時間了,那時候我的詞彙表中可能還沒有 “水帖” 這個詞。

爲此,我受到了一個早期項目的啓發,該項目基於區塊鏈初創公司將投資者的臉投影到 3D 立方體上讓我想起了以前的互聯網,那時一切都很奇怪。

好漢不提當年勇。所以今天,我將討論如何管理我自己的新項目:sqlcss.xyz[1]

顧名思義,這就是使用 CSS 連接數據庫的方式。不幸的是,它只能在 Chrome 中工作,但你可以提供任何你喜歡的 SQLite 數據庫,並通過 CSS 查詢它。

它是如何工作的?

首先我們需要用到一組被親切地稱爲 Houdini[2] 的 api,它讓你的瀏覽器能夠通過 Javascript 對象模型來控制 CSS。換言之,這意味着您可以定製 CSS 樣式、添加定製屬性,等等。

可能這個作品最大的特性是 CSS Paint Worklet[3],它允許你在一個元素上 “繪製”,就像你知道和喜歡的畫布一樣,並讓瀏覽器把它當作 CSS 中的圖像。這裏有一些例子可以用來演示 Houdini[4]。

然而,這個工作集只提供了 Worker API 的一個子集,而且畫布上下文本身也被大量剝離。這樣做的實際結果是,您的自定義 CSS 繪製代碼提供了一個比您預期的更小的沙盒。

這意味着什麼? 沒有網絡訪問權限,因此可以和 fetch 和 XmlHttpRequest 說再見了。在繪製上下文上沒有 drawText 功能。其他各種 JS api 也消失了,以防你希望解決這些問題。

不過,不用擔心。並非一切都完了。讓我們把它分解成幾個步驟。


1. 設置數據庫

這必須是第一步,以便理解概念證明是否可行。

首先我們會藉助於 sql.js[5]。它實際上是一個通過 emscripten 編譯成 WebAssembly 和老式 ASM.js 的 SQLite 版本。不幸的是,我們不能使用 WASM 版本,因爲它必須通過網絡獲取二進制文件。ASM 版本沒有這個限制,因爲所有的代碼都可以在一個模塊中使用。

雖然 PaintWorklet 限制了 worker 內部的網絡訪問,但你仍然可以導入代碼,只要它是一個 ES6 模塊。這意味着文件中必須有一個導出語句。不幸的是,sql.js 沒有 ES6 的版本,所以我自己修改了 sql.js,使其能夠順利的被 import 進入項目。

現在到了關鍵時刻: 我可以在我的工作包中建立一個數據庫嗎?

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)

正如預期的那樣,結果已經出來了。不過,渲染展示通過 CSS 查詢數據庫的結果會更好。

3. 渲染結果,最簡單的方法

我認爲這就像在畫布上寫文本一樣。這有多難,對吧?

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);
  }
}

不,那樣就太簡單了。這裏的上下文與畫布元素的上下文不同,它只提供了功能的一個子集。

當然,它仍然可以繪製路徑和曲線,所以缺乏方便的 API 是一個障礙,但這一切都不是問題。

4. 不使用文本 API 創建文本

幸運的是,我們可以藉助於 opentype.js[6] 所提供的解決方案。它可以解析一個字體文件,然後,給定一個文本字符串,生成每個字符的字母形式。這個操作的實際結果是一個表示字符串的路徑對象,然後可以將其呈現到上下文中。

這次我不必修改 opentype 庫來導入它,因爲它已經可以從 JSPM[7] 中獲得。所以,如果你給 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')

但這裏有一個問題——它想通過網絡加載字體,而我不能這樣做! 嗨, 挫敗了!

…… 而且? 它還有一個接受數組緩衝區的解析方法。我將用 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)

我告訴過你 worklet 也沒有處理 base64 字符串的 api 嗎? atob 和 btoa 都沒有! 我也不得不爲此找到一個普通的 JS 實現。

我把這段代碼放在它自己的文件中,因爲它不太符合人體工程學…… 必須在剩下的代碼旁邊使用大約 200kb 的編碼字體字符串。

這就是我爲何要濫用 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 來查詢數據庫,那就更好了。事實上,這是我們可以在 Paint Worker 的上下文之外與其通信的唯一方式,因爲沒有與 Web worker 一樣的消息傳遞 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。你會注意到我並沒有滿足於此。在進行了一些重構之後,又進行了一些更改。

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 數據庫作爲 base64 編碼的數據 URI 提供。data URI 只是爲了顯示並確保它對 DOM 是有效的, 我將在 Worker 層面解析這些東西。

最後一步是使其更易於查詢,因爲否則您必須進入調試器來操作元素的 CSS。

8. 編寫查詢語句

這可能是項目中最簡單的部分。自定義屬性對於分號有一點問題,而 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 連接到數據庫並沒有太大關係,所以我認爲在這裏談論這個問題並不合適,但如果你想進一步瞭解這個 "荒謬" 的概念,git 上的代碼都是可用的 [8]。

譯者補充

  1. SQLite 是由 C 語言編寫的嵌入式輕量級數據庫,廣泛用於 IOS 和 Android 的 App 中。若有讀者想要在瀏覽器端應用 SQLite,推薦使用 SQLite 的 WebAssembly 版 --sql.js,基本與 SQLite 的使用沒有太多區別。在實際項目使用中,還需特別注意 SQLite 的安全性問題。

  2. 爲方便展示,特此截取本文作者所實現的 CSS 連接 SQLite 的部分 demo 如下:

  1. 關於文中所提到的 jspm, 這裏引用其官網的介紹如下: jspm 爲導入映射提供了模塊 CDN 和包管理,允許任何來自 NPM 的包都可以直接在瀏覽器中加載,無需進一步的處理。

參考資料

[1]

sqlcss.xyz: https://www.sqlcss.xyz

[2]

CSS Houdini: https://developer.mozilla.org/en-US/docs/Web/Guide/Houdini

[3]

CSS Paint Worklet: https://developer.mozilla.org/en-US/docs/Web/API/PaintWorklet

[4]

houdini how: https://houdini.how/

[5]

sql.js: https://sql.js.org/#/

[6]

opentype.js: https://opentype.js.org/

[7]

jspm: https://jspm.org/

[8]

sqlcss git: https://git.sr.ht/~mrlee/sqlcss

奇舞團是 360 集團最大的大前端團隊,代表集團參與 W3C 和 ECMA 會員(TC39)工作。奇舞團非常重視人才培養,有工程師、講師、翻譯官、業務接口人、團隊 Leader 等多種發展方向供員工選擇,並輔以提供相應的技術力、專業力、通用力、領導力等培訓課程。奇舞團以開放和求賢的心態歡迎各種優秀人才關注和加入奇舞團。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/dfmQUPS7_FJZ1sDK8_HS8w