小試前端 REST 資料庫 - Supabase
3 |
最近玩了自架 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 布丁布丁吃布丁
主要還是想要跨裝置共享資料吧,這樣就一定要後端資料庫了