提到 Pleroma,很多前端同学的第一反应是:这是不是又一个 Vue 组件库?其实恰恰相反,Pleroma 是一个用 Elixir 编写的服务端项目,它轻量、资源占用低,单核小机器也能流畅运行,并且完整支持 ActivityPub 联邦协议。真正的工程化难点在于:如何用 Vue 3 写一个体验良好的客户端,去消费 Pleroma 暴露出来的 REST API 和 WebSocket 流。这篇文章就把整套链路捋清楚。

一、先厘清架构:Pleroma 前后端边界在哪里
Pleroma 自带一个官方 Web 前端,早期是 Pleroma-FE,后来主推 FE 流派逐渐转向可替换方案,服务端只负责数据与联邦。它的对外接口分为两层:一层是 ActivityPub 本身(面向服务器间联邦,用 JSON-LD 签名交互),另一层是 Mastodon 兼容 REST API(面向客户端应用)。对 Vue 3 客户端来说,几乎永远只需要关心后者,因为浏览器直接处理 ActivityPub 的签名机制既不安全也不现实。
换句话说,你在 Vue 3 项目里要做的事情是:通过 https://你的实例域名/api/v1/ 下的接口完成登录、发帖、拉取时间线、处理通知,再通过 /api/v1/streaming 的 WebSocket 通道实现实时更新。Pleroma 在这套兼容 API 上还扩展了不少私有端点,比如表情反应、聊天消息等,都挂在 /api/v1/pleroma/ 前缀下。理解了这个分层,后面的工程化就顺理成章了。
二、OAuth 授权接入:Composition API 封装认证层
Pleroma 使用标准 OAuth 2.0,客户端需要先在实例上注册一个应用,拿到 client_id 和 client_secret,再引导用户授权换取 access_token。开发阶段可以先手动 curl 注册:
curl -X POST https://你的实例/api/v1/apps \
-H "Content-Type: application/json" \
-d '{
"client_name": "vue3-pleroma-client",
"redirect_uris": "urn:ietf:wg:oauth:2.0:oob",
"scopes": "read write follow"
}'拿到凭据后,在 Vue 3 里建议把整个授权流程封装成一个独立的 composable,这样登录状态可以在任意组件间共享。核心思路是用 ref 持有 token,持久化交给 localStorage,并在 token 失效时统一处理刷新:
// composables/useAuth.js
import { ref, computed } from 'vue'
const token = ref(localStorage.getItem('pleroma_token') || '')
const apiUrl = 'https://你的实例/api/v1'
export function useAuth() {
const isLoggedIn = computed(() => token.value !== '')
async function login(clientId, clientSecret, username, password) {
const body = new URLSearchParams({
grant_type: 'password',
client_id: clientId,
client_secret: clientSecret,
username: username,
password: password,
scope: 'read write follow'
})
const res = await fetch(apiUrl + '/oauth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body
})
if (!res.ok) throw new Error('授权失败,请检查凭据')
const data = await res.json()
token.value = data.access_token
localStorage.setItem('pleroma_token', data.access_token)
}
function logout() {
token.value = ''
localStorage.removeItem('pleroma_token')
}
function authHeaders() {
return { Authorization: 'Bearer ' + token.value }
}
return { token, isLoggedIn, login, logout, authHeaders, apiUrl }
}这套封装的好处是职责单一:网络层组件不需要知道 token 怎么来的,只管调用 authHeaders()。如果项目用了 axios,也可以换成拦截器方案,把 Authorization 头统一注入,并在 401 响应时自动跳转登录页。两种方式都可行,composable 方案胜在与 Composition API 心智模型一致,迁移成本低。
三、时间线数据流与 WebSocket 实时流
时间线是社交客户端的骨架。Pleroma 的 /api/v1/timelines/home 返回按时间倒序的 status 数组,带分页链接。在 Vue 3 里推荐用「本地响应式数组 + 追加加载」的模式,配合 nextLink 记录分页游标:
// composables/useTimeline.js
import { ref } from 'vue'
import { useAuth } from './useAuth'
export function useTimeline(path) {
const { apiUrl, authHeaders } = useAuth()
const statuses = ref([])
let nextUrl = apiUrl + path
async function loadMore() {
const res = await fetch(nextUrl, { headers: authHeaders() })
const data = await res.json()
// Link 响应头里包含下一页地址
nextUrl = extractNextLink(res.headers.get('Link')) || null
statuses.value.push(...data)
}
function prepend(status) {
statuses.value.unshift(status)
}
return { statuses, loadMore, prepend }
}实时更新则依赖 Pleroma 的 streaming 端点。WebSocket 连接建立后,向服务端订阅 home 流,所有新帖子和删除事件都会以 JSON 消息推送过来。写成一个独立的 composable 之后,组件里只需要一行代码就能把新状态塞进时间线顶部:
// composables/useStreaming.js
import { onMounted, onUnmounted } from 'vue'
import { useAuth } from './useAuth'
export function useStreaming(onStatus) {
const { token } = useAuth()
const wsUrl = 'wss://你的实例/api/v1/streaming'
let socket = null
onMounted(() => {
socket = new WebSocket(wsUrl + '?access_token=' + token.value)
socket.onopen = () => socket.send(JSON.stringify({ type: 'subscribe', stream: 'user' }))
socket.onmessage = (event) => {
const payload = JSON.parse(event.data)
if (payload.event === 'update') {
onStatus(JSON.parse(payload.payload))
}
}
})
onUnmounted(() => socket && socket.close())
}四、性能优化:虚拟滚动与发布状态管理
联邦时间线刷起来很快,几百条 status 一旦全部渲染,DOM 数量会迅速失控,移动端尤其明显。工程上的标准答案是虚拟滚动:只渲染视口内的帖子,配合分页无限加载。可选方案有 vueuse 的 useVirtualList,或者直接引入 vue-virtual-scroller。需要注意的是 status 高度不一致,务必选用支持动态高度的模式,否则滚动定位会跳。
发帖体验上也有讲究。Pleroma 的发布接口支持 idempotency_key 头,用它做幂等键可以避免弱网环境下用户重试导致的重复发帖,这一点在生产环境非常关键。同时建议用 provide/inject 把发帖函数注入到布局层,让任何位置的 composer 组件都能复用,而不是各自维护一份提交逻辑。媒体上传走 /api/v2/media,先上传拿 media_id 再随帖提交,进度可以用 XMLHttpRequest 或 axios 的 onUploadProgress 回调驱动进度条。
五、构建与部署配置
最后是打包环节。Pleroma 实例通常由管理员用 nginx 反代,纯前端客户端可以用 hash 路由避开服务端路由配置,或者部署到任意静态托管平台。要注意 CORS:如果你不把前端和实例放在同一域名下,就需要在 Pleroma 配置里放行你的前端域名,否则浏览器会直接拦截请求。Vite 构建时把实例地址抽成环境变量 VITE_API_BASE,方便切换开发环境和生产环境。整体来看,用 Vue 3 对接 Pleroma 的工作量并不大,难点集中在授权流程、流式更新和长列表性能这三块,把这几个 composable 写扎实,一个轻量级的 ActivityPub 客户端就成型了。
Vue 3PleromaActivityPub修改时间:2026-09-03 17:19:06