---
tags:
- backend
- pocketbase
- tutorial
- sqlite
- api
aliases:
- PocketBase 教學
- PocketBase 實作
created: 2024-12-22
updated: 2024-12-22
---
# PocketBase 入門到實作:從零到上線的完整教學

## 前言
上一篇介紹了各種 Supabase 替代品,這篇要深入 PocketBase 的實際操作。從安裝、設定、前端整合到部署上線,完整走一遍流程。
PocketBase 的特點是簡單直接。不需要 Docker,不需要資料庫安裝,下載一個檔案就能跑。這讓它特別適合:
- 想快速驗證想法的個人開發者
- 學習後端開發的初學者
- 需要輕量後端的小型專案
- 不想維護複雜基礎設施的團隊
這篇教學會涵蓋:
1. 安裝與啟動
2. Admin 後台操作
3. 資料庫設計
4. 前端整合(含完整程式碼)
5. 認證系統
6. 自訂 API
7. 部署到線上
8. 實戰案例:待辦清單應用
---
## 環境準備
PocketBase 是單一執行檔,不需要預先安裝任何東西。支援的作業系統:
- Linux(amd64、arm64)
- macOS(Intel、Apple Silicon)
- Windows
---
## 第一步:安裝與啟動
### 方法一:直接下載
```bash
# macOS (Apple Silicon)
wget https://github.com/pocketbase/pocketbase/releases/download/v0.28.4/pocketbase_0.28.4_darwin_arm64.zip
# macOS (Intel)
wget https://github.com/pocketbase/pocketbase/releases/download/v0.28.4/pocketbase_0.28.4_darwin_amd64.zip
# Linux
wget https://github.com/pocketbase/pocketbase/releases/download/v0.28.4/pocketbase_0.28.4_linux_amd64.zip
# 解壓縮
unzip pocketbase_*.zip
# 啟動
./pocketbase serve
```
啟動後會看到:
```
2024/01/01 12:00:00 Server started at http://127.0.0.1:8090
- REST API: http://127.0.0.1:8090/api/
- Admin UI: http://127.0.0.1:8090/_/
```
### 方法二:Docker
如果偏好容器化部署:
```dockerfile
FROM alpine:latest
ARG PB_VERSION=0.28.4
RUN apk add --no-cache unzip ca-certificates
ADD https://github.com/pocketbase/pocketbase/releases/download/v${PB_VERSION}/pocketbase_${PB_VERSION}_linux_amd64.zip /tmp/pb.zip
RUN unzip /tmp/pb.zip -d /pb/
EXPOSE 8080
CMD ["/pb/pocketbase", "serve", "--http=0.0.0.0:8080"]
```
```bash
docker build -t pocketbase .
docker run -d -p 8080:8080 -v $(pwd)/pb_data:/pb/pb_data pocketbase
```
### 首次設定
開啟 `http://127.0.0.1:8090/_/` 會看到管理員帳號設定頁面。填入 email 和密碼後,就能進入 Admin 後台。
這個帳號是超級管理員(Superuser),擁有所有權限。實際專案中請使用強密碼。
---
## 第二步:認識 Admin 後台
Admin 後台分為幾個主要區塊:
### Collections(資料集合)
類似資料庫的 Table。每個 Collection 包含:
- **欄位定義**:支援多種類型(Text、Number、Bool、Date、File、Relation 等)
- **API 規則**:控制 CRUD 權限
- **索引設定**:優化查詢效能
PocketBase 預設有兩個系統 Collection:
- `_superusers`:管理員帳號
- `users`:一般使用者(Auth Collection)
### Logs
查看 API 請求紀錄、錯誤訊息。
### Settings
全域設定,包含:
- 應用程式名稱
- SMTP 設定(用於發送驗證信)
- 檔案儲存設定(本地或 S3)
- 認證選項
- 備份設定
---
## 第三步:建立第一個 Collection
以部落格文章為例,建立 `posts` Collection:
### 在 Admin 後台操作
1. 點擊「New collection」
2. 輸入名稱:`posts`
3. 類型選擇:「Base」(一般資料集合)
4. 新增欄位:
| 欄位名稱 | 類型 | 設定 |
|---------|------|------|
| title | Text | Required |
| content | Editor | Required |
| slug | Text | Required, Unique |
| status | Select | Options: draft, published |
| author | Relation | 關聯到 users |
| cover | File | 允許圖片類型 |
| published_at | DateTime | - |
### 設定 API 規則
在 Collection 設定的「API Rules」區塊:
```javascript
// List/Search Rule - 只有已發布的文章對外公開
status = "published"
// View Rule - 同上,或作者本人可看草稿
status = "published" || author = @request.auth.id
// Create Rule - 只有登入使用者可建立
@request.auth.id != ""
// Update Rule - 只有作者可編輯
author = @request.auth.id
// Delete Rule - 只有作者可刪除
author = @request.auth.id
```
這些規則用類似 SQL WHERE 的語法,`@request.auth` 代表當前登入的使用者。
---
## 第四步:前端整合
### 安裝 SDK
```bash
npm install pocketbase
```
### 初始化
```javascript
import PocketBase from 'pocketbase';
const pb = new PocketBase('http://127.0.0.1:8090');
// 如果在 Node.js 環境,需要處理 localStorage
// pb.authStore = new BaseAuthStore();
```
### CRUD 操作
#### 建立記錄
```javascript
const record = await pb.collection('posts').create({
title: '我的第一篇文章',
content: '<p>這是內容...</p>',
slug: 'my-first-post',
status: 'draft',
author: pb.authStore.record.id
});
console.log(record.id); // 自動生成的 ID
```
#### 查詢記錄
```javascript
// 取得列表(分頁)
const resultList = await pb.collection('posts').getList(1, 20, {
filter: 'status = "published"',
sort: '-created',
expand: 'author'
});
console.log(resultList.items);
console.log(resultList.totalItems);
console.log(resultList.totalPages);
// 取得單筆
const record = await pb.collection('posts').getOne('RECORD_ID', {
expand: 'author'
});
// 用其他欄位查詢
const post = await pb.collection('posts').getFirstListItem(
`slug = "my-first-post"`
);
```
#### 更新記錄
```javascript
const updated = await pb.collection('posts').update('RECORD_ID', {
title: '更新後的標題',
status: 'published',
published_at: new Date().toISOString()
});
```
#### 刪除記錄
```javascript
await pb.collection('posts').delete('RECORD_ID');
```
### 檔案上傳
```javascript
// 建立 FormData
const formData = new FormData();
formData.append('title', '帶圖片的文章');
formData.append('content', '<p>內容</p>');
formData.append('slug', 'post-with-image');
formData.append('status', 'draft');
formData.append('author', pb.authStore.record.id);
formData.append('cover', fileInput.files[0]); // File 物件
const record = await pb.collection('posts').create(formData);
// 取得檔案 URL
const coverUrl = pb.files.getUrl(record, record.cover);
// 加上尺寸參數(自動縮放)
const thumbUrl = pb.files.getUrl(record, record.cover, { thumb: '300x200' });
```
### 即時訂閱
PocketBase 支援 SSE(Server-Sent Events)實現即時資料同步:
```javascript
// 訂閱整個 Collection
pb.collection('posts').subscribe('*', function (e) {
console.log(e.action); // 'create' | 'update' | 'delete'
console.log(e.record);
});
// 訂閱特定記錄
pb.collection('posts').subscribe('RECORD_ID', function (e) {
console.log('記錄已更新:', e.record);
});
// 取消訂閱
pb.collection('posts').unsubscribe('*');
pb.collection('posts').unsubscribe('RECORD_ID');
pb.collection('posts').unsubscribe(); // 取消此 Collection 所有訂閱
```
---
## 第五步:認證系統
### 使用者註冊
```javascript
const user = await pb.collection('users').create({
email: 'user@example.com',
password: '12345678',
passwordConfirm: '12345678',
name: '使用者名稱'
});
// 發送驗證信(需要先設定 SMTP)
await pb.collection('users').requestVerification('user@example.com');
```
### 登入
```javascript
// Email + 密碼
const authData = await pb.collection('users').authWithPassword(
'user@example.com',
'12345678'
);
console.log(pb.authStore.isValid); // true
console.log(pb.authStore.token); // JWT
console.log(pb.authStore.record); // 使用者資料
```
### OAuth2 登入
需要先在 Admin 後台設定 OAuth2 提供者(Settings → Auth providers)。
```javascript
// 自動處理彈窗流程
const authData = await pb.collection('users').authWithOAuth2({
provider: 'google'
});
```
### 登出
```javascript
pb.authStore.clear();
```
### 檢查登入狀態
```javascript
if (pb.authStore.isValid) {
console.log('已登入:', pb.authStore.record.email);
} else {
console.log('未登入');
}
// 監聽狀態變化
pb.authStore.onChange((token, record) => {
console.log('認證狀態改變');
});
```
### 刷新 Token
```javascript
// Token 即將過期時刷新
if (pb.authStore.isValid) {
await pb.collection('users').authRefresh();
}
```
---
## 第六步:自訂 API 路由
有時候內建的 CRUD API 不夠用,需要自訂邏輯。
### 使用 JavaScript Hooks
在 `pb_hooks` 目錄下建立 `.js` 檔案:
```javascript
// pb_hooks/custom_routes.pb.js
// 自訂 GET 路由
routerAdd("GET", "/api/custom/stats", (e) => {
// 執行 SQL 查詢
const result = $app.db()
.newQuery(`
SELECT
COUNT(*) as total,
SUM(CASE WHEN status = 'published' THEN 1 ELSE 0 END) as published
FROM posts
`)
.one();
return e.json(200, result);
}, $apis.requireAuth()); // 需要認證
// 自訂 POST 路由
routerAdd("POST", "/api/custom/publish/{id}", (e) => {
const id = e.request.pathValue("id");
const record = $app.findRecordById("posts", id);
// 檢查權限
if (record.get("author") !== e.auth.id) {
throw new ForbiddenError("只有作者可以發布");
}
record.set("status", "published");
record.set("published_at", new Date().toISOString());
$app.save(record);
return e.json(200, record);
}, $apis.requireAuth());
```
### Hooks(事件鉤子)
```javascript
// pb_hooks/hooks.pb.js
// 建立記錄前
onRecordCreate((e) => {
// 自動設定 slug
if (!e.record.get("slug")) {
const title = e.record.get("title");
const slug = title.toLowerCase().replace(/\s+/g, '-');
e.record.set("slug", slug);
}
return e.next();
}, "posts");
// 建立記錄後
onRecordAfterCreateSuccess((e) => {
console.log("新文章建立:", e.record.id);
// 可以在這裡發送通知、更新快取等
}, "posts");
// 刪除記錄前
onRecordDelete((e) => {
// 阻止刪除已發布的文章
if (e.record.get("status") === "published") {
throw new BadRequestError("無法刪除已發布的文章");
}
return e.next();
}, "posts");
```
---
## 第七步:部署到線上
### 方法一:VPS 直接部署
適合:個人專案、小型應用
```bash
# 1. 上傳執行檔和 pb_data 到伺服器
scp pocketbase user@server:/opt/pocketbase/
scp -r pb_data user@server:/opt/pocketbase/
# 2. SSH 到伺服器
ssh user@server
# 3. 設定 systemd 服務
sudo nano /etc/systemd/system/pocketbase.service
```
```systemd
[Unit]
Description=PocketBase
After=network.target
[Service]
Type=simple
User=www-data
WorkingDirectory=/opt/pocketbase
ExecStart=/opt/pocketbase/pocketbase serve yourdomain.com
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
```
```bash
# 4. 啟動服務
sudo systemctl daemon-reload
sudo systemctl enable pocketbase
sudo systemctl start pocketbase
# 5. 檢查狀態
sudo systemctl status pocketbase
```
使用 `yourdomain.com` 作為參數時,PocketBase 會自動處理 Let's Encrypt 憑證。
### 方法二:Docker + Nginx
適合:需要更多控制、多服務架構
```yaml
# docker-compose.yml
version: '3.8'
services:
pocketbase:
build: .
container_name: pocketbase
volumes:
- ./pb_data:/pb/pb_data
- ./pb_hooks:/pb/pb_hooks
restart: unless-stopped
networks:
- web
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
- ./certs:/etc/nginx/certs
depends_on:
- pocketbase
networks:
- web
networks:
web:
```
### 方法三:Railway / Fly.io / Render
這些 PaaS 平台支援 Docker 部署,適合不想管理伺服器的情況。
以 Fly.io 為例:
```toml
# fly.toml
app = "my-pocketbase"
primary_region = "nrt" # 東京
[build]
dockerfile = "Dockerfile"
[mounts]
source = "pb_data"
destination = "/pb/pb_data"
[http_service]
internal_port = 8080
force_https = true
```
```bash
fly launch
fly deploy
```
---
## 實戰案例:待辦清單應用
把前面學的整合起來,建立一個完整的待辦清單應用。
### 資料結構
建立 `todos` Collection:
| 欄位 | 類型 | 設定 |
|------|------|------|
| title | Text | Required |
| completed | Bool | Default: false |
| user | Relation | 關聯 users, Required |
| due_date | DateTime | - |
API 規則:
```javascript
// 所有操作都限制為擁有者
List: user = @request.auth.id
View: user = @request.auth.id
Create: @request.auth.id != "" && @request.body.user = @request.auth.id
Update: user = @request.auth.id
Delete: user = @request.auth.id
```
### 前端程式碼(Vue 3 範例)
```vue
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import PocketBase from 'pocketbase'
const pb = new PocketBase('http://127.0.0.1:8090')
const todos = ref([])
const newTodo = ref('')
const loading = ref(false)
// 載入待辦事項
async function loadTodos() {
loading.value = true
try {
const records = await pb.collection('todos').getFullList({
sort: '-created',
filter: `user = "${pb.authStore.record.id}"`
})
todos.value = records
} finally {
loading.value = false
}
}
// 新增待辦
async function addTodo() {
if (!newTodo.value.trim()) return
const record = await pb.collection('todos').create({
title: newTodo.value,
completed: false,
user: pb.authStore.record.id
})
todos.value.unshift(record)
newTodo.value = ''
}
// 切換完成狀態
async function toggleTodo(todo) {
const updated = await pb.collection('todos').update(todo.id, {
completed: !todo.completed
})
const index = todos.value.findIndex(t => t.id === todo.id)
todos.value[index] = updated
}
// 刪除待辦
async function deleteTodo(todo) {
await pb.collection('todos').delete(todo.id)
todos.value = todos.value.filter(t => t.id !== todo.id)
}
// 即時同步
let unsubscribe
onMounted(async () => {
await loadTodos()
// 訂閱變更
unsubscribe = await pb.collection('todos').subscribe('*', (e) => {
if (e.action === 'create') {
// 避免重複新增自己建立的
if (!todos.value.find(t => t.id === e.record.id)) {
todos.value.unshift(e.record)
}
} else if (e.action === 'update') {
const index = todos.value.findIndex(t => t.id === e.record.id)
if (index !== -1) {
todos.value[index] = e.record
}
} else if (e.action === 'delete') {
todos.value = todos.value.filter(t => t.id !== e.record.id)
}
})
})
onUnmounted(() => {
unsubscribe?.()
})
</script>
<template>
<div class="todo-app">
<h1>待辦清單</h1>
<form @submit.prevent="addTodo" class="add-form">
<input
v-model="newTodo"
placeholder="新增待辦事項..."
:disabled="loading"
/>
<button type="submit" :disabled="loading || !newTodo.trim()">
新增
</button>
</form>
<ul class="todo-list">
<li
v-for="todo in todos"
:key="todo.id"
:class="{ completed: todo.completed }"
>
<input
type="checkbox"
:checked="todo.completed"
@change="toggleTodo(todo)"
/>
<span>{{ todo.title }}</span>
<button @click="deleteTodo(todo)" class="delete">×</button>
</li>
</ul>
</div>
</template>
<style scoped>
.todo-app {
max-width: 500px;
margin: 0 auto;
padding: 20px;
}
.add-form {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.add-form input {
flex: 1;
padding: 10px;
}
.todo-list {
list-style: none;
padding: 0;
}
.todo-list li {
display: flex;
align-items: center;
gap: 10px;
padding: 10px;
border-bottom: 1px solid #eee;
}
.todo-list li.completed span {
text-decoration: line-through;
color: #999;
}
.delete {
margin-left: auto;
background: #ff4444;
color: white;
border: none;
padding: 5px 10px;
cursor: pointer;
}
</style>
```
---
## 常見問題
### Q: 資料如何備份?
```bash
# 停止服務後複製 pb_data
sudo systemctl stop pocketbase
cp -r /opt/pocketbase/pb_data /backup/pb_data_$(date +%Y%m%d)
sudo systemctl start pocketbase
# 或使用 SQLite 線上備份(不需停止服務)
sqlite3 /opt/pocketbase/pb_data/data.db ".backup /backup/data_$(date +%Y%m%d).db"
```
### Q: 如何重設管理員密碼?
```bash
./pocketbase superuser upsert admin@example.com newpassword
```
### Q: 效能瓶頸在哪?
SQLite 的寫入鎖是主要限制。如果需要高併發寫入,考慮:
- 使用佇列處理寫入請求
- 評估是否真的需要 PocketBase(可能 PostgreSQL 更適合)
### Q: 可以用在生產環境嗎?
可以,但要評估:
- 預期流量(SQLite 適合中低流量)
- 資料重要性(確保備份策略)
- 團隊維護能力
---
## 進階學習資源
- [[PocketBase快速搭建指南]] - 更多部署選項
- [[PocketBase自訂API路由]] - 深入 API 擴充
- [[PocketBase認證與權限設定]] - 完整的權限控制
- [官方文件](https://pocketbase.io/docs/)
- [GitHub 討論區](https://github.com/pocketbase/pocketbase/discussions)
---
## 結語
PocketBase 不是要取代所有後端方案,而是提供一個極簡的選擇。當你的需求是快速驗證想法、建立個人專案、或者只是需要一個簡單的後端時,它能讓你在幾分鐘內就有一個功能完整的後端服務。
這篇教學涵蓋了從入門到實戰的主要知識點。實際專案中可能還會遇到其他問題,但有了這個基礎,查閱官方文件或社群討論應該都能找到解答。
祝開發順利。