最近玩了自架 ChatGPT 網站的開源專案 Chatbot UI,網站以 React 前端程式為核心,1.0 原以 localStorage 儲存資料,基於安全疑慮、大小限制、無法跨機器共用等因素,改版時將資料改存到後端,用了一個我沒看過的資料庫 - Supabase

Supabase是一個開源的後端服務平台(取代 Google 的閉源服務 - Firebase),核心概念是把 PostgreSQL 包成 RESTful API/ GraphQL,允許 JavaScript 從瀏覽器端讀寫資料庫內容;但 Supabase 功能不只於此,它還整合前端常用的使用者驗證、資料更新時主動通知、Embedding 向量資料搜尋... 等功能。Supabase 提供的這些功能,我自己是習慣開發自訂 Web API 實作,不喜歡資料庫直接對外讓前端程式自由操作(有安全及效能疑慮)。但將資料操作邏輯寫在前端的概念簡單粗暴,不用多花心力搞一套專屬 API,也可選擇雲端版服務(連後端人員都省了),加上同一套程式前後端都能跑... 猜想這是前端工程師愛用的原因吧。

雖然我個人還是覺得:放任前端自由操作資料庫,對安全與效能是件危險的事,但因緣際會碰上了,老狗再學點新把戲長見識唄。

我演練了 CLI 工具安裝、新增專案、建資料表到用 JavaScript 程式讀寫資料的過程,記錄重點如下:

安裝 Supabase CLI

Supabase 有雲端服務 (有免費也有付費方案),但也可以跑 Docker 容器在地端執行,我則是在 Linux Ubuntu 22.04 跑的測試,先裝好 Docker 服務,接著安裝 Supabase CLI (Linux 可以用 Homebrew 或下載 .deb/.rpm 安裝)。

建立專案

建立專案資料夾,在目錄下執行 supabase init,CLI 會建立 supabase 子資料夾,執行 supabase start CLI 會下載並為這個專案建立 12 個 Docker 容器 [1]、Web API 在 54321 Port [2],管理介面在 54323 Port [3],anon key 與 service_role Key [4] 存取時會用到,另外還建了五個 Volume [5] 用來放資料:

Supabase 使用 anon 及 service_role 兩支 JWT 長效金鑰區隔權限,anon 通常配合 RLS(Row Level Security) 使用,每個人只能讀寫自己寫入的資料,service_role 則能讀寫所有資料。故 anon Key 類似公鑰,可明碼寫入網頁送到前端,反正讀寫資料時會一併檢查登入身分,判斷是否有權存取。而 sevice_role Key 則如同系統管理密碼,需嚴加保護。一般只在後端使用(為了怕有人失手 Push 到 Github,Github 有加了防呆檢查),sevice_role Key 一旦外流等同資料完全對外公開,務必謹慎處理。(這也是我不愛資料庫直接對外的理由)

建立資料表

接著我打算建立一個測試資料表 players。執行 supabase migration new create_players_table 會建立 supabase/migrations/<timestamp>_create_players_table.sql 空白檔,填入新增資料表 SQL 指令:

CREATE TABLE players (
    id SERIAL PRIMARY KEY,
    name VARCHAR(16) UNIQUE NOT NULL,
    score INTEGER NOT NULL,
    regdate TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

supabase/seed.sql 填入新增資料指令:

INSERT INTO players (name, score) VALUES
('Jeffrey', 32767);

接著執行 supabase db reset 初始化資料庫、建立資料表並寫入資料:

使用 http://127.0.0.1:54323/ 管理介面可查到寫入的內容:(介面有提醒目前資料表允許匿名讀寫,實際應用常設為 Row Level Security)

node.js 讀寫資料

再來測試用 Node.js 跑 JavaScript 讀取資料。開始前先 npm install @supabase/supabase-js 安裝程式庫,讀取程式如下:

import { createClient } from '@supabase/supabase-js'
// TODO: read from configuration file
const apiUrl = 'http://127.0.0.1:54321';
const anonKey = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.***';
// Create a single supabase client for interacting with your database
const supabase = createClient(apiUrl, anonKey);
// Read data from players table
const readData = async () => {
  const { data, error } = await supabase
    .from('players')
    .select('*')
  if (error) {
    console.error('Error fetching data:', error)
    return
  }
  console.log('Data:', data)
}

readData();

讀取成功!

把程式搬進網頁執行

我們將上面的程式碼稍作修改,搬進網頁執行看看:參考

<!DOCTYPE html>
<html>

<head>
    <title>Read Data</title>
    <script src="https://unpkg.com/@supabase/supabase-js@2"></script>
</head>

<body>
    <pre></pre>
    <script>
        const { createClient } = supabase;
        const apiUrl = 'http://127.0.0.1:54321';
        const anonKey = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.***';
        const _supabase = createClient(apiUrl, anonKey);
        const readData = async () => {
            const { data, error } = await _supabase
                .from('players')
                .select('*')
            if (error) {
                alert('Error fetching data:', error)
                return
            }
            document.querySelector('pre').textContent = JSON.stringify(data, null, 2)
        }
        readData();        
    </script>
</body>

</html>

薑! 薑! 薑! 薑~~~ 只要前端能存取 54321 Port,同一段程式可以放進瀏覽器執行,一魚兩吃。

由以上展示,有點能了解為什麼 Supabase 這種解決方案對前端工程師特別有吸引力。

REST API

最後,有一行 curl 呼叫 REST API 範例結束這回合~

curl 'https://<PROJECT_REF>.supabase.co/rest/v1/todos' \
-H "apikey: <ANON_KEY>" \
-H "Authorization: Bearer <ANON_KEY>"

後記:chatbot-ui 目前這個搭配 Supabase 的版本,架構與部署有點複雜,很難做到即插即用。作者有提到目前已在研發 Sqlite 版(這類應用,Sqlite 也是我心中的首選)預計幾週後推出,我決定先不跟 Subabase 糾纏,等熟悉的 Sqlite 版出來再玩。

Introduce to the front-end data service solution - Supabase.


Comments

# by

不知道為何放棄 localStorage 時不改用 IndexedDB ps: localStorate 應該是 localStorage

# by Jeffrey

to 貴,謝分享,學到新名詞 IndexedDB。錯字已校正。

# by 布丁布丁吃布丁

主要還是想要跨裝置共享資料吧,這樣就一定要後端資料庫了

Post a comment