Vue 3 项目要接入去中心化图片分享,Pixelfed 是当前比较务实的选择。它不像商业图床那样提供封闭 SDK,而是基于 ActivityPub 协议开放实例 API,这意味着前端需要自己处理授权、请求封装和状态同步。本文将从实际工程角度拆解这一过程,先梳理 Pixelfed 的 API 特点,再给出 Vue 3 中的可复用封装,最后讨论上传与时间线的性能优化。

Pixelfed API 特点与工程化前的准备
Pixelfed 是一个基于 ActivityPub 的联邦图片分享平台,每个实例独立部署,用户可以通过自己所在实例与其他实例互动。与 Mastodon 类似,它提供 REST API,常见端点包括 /api/v1/accounts/verify_credentials、/api/v1/statuses、/api/v1/media。前端接入前需要明确目标实例地址,例如 https://pixelfed.social,并申请 OAuth 应用获取 client_id 和 client_secret。这里要注意:Pixelfed 的 OAuth 流程与 Mastodon 基本一致,但个别实例可能关闭注册或调整权限范围,调用前应阅读实例的 /api/v1/instance 返回信息。
工程化接入的第一个难点是跨域。如果 Vue 3 开发服务器直接请求远程 Pixelfed 实例,浏览器会因 CORS 策略拦截。虽然部分实例允许跨域,但生产环境更稳妥的做法是通过 Vite 开发代理或自建 BFF 层转发请求。开发阶段可以在 vite.config.js 中配置 server.proxy,将 /pixelfed 开头的请求代理到目标实例,这样前端代码使用相对路径,避免暴露真实实例地址。
第二个难点是令牌管理。Pixelfed 使用 Bearer Token 进行身份认证,访问令牌有过期时间,需要通过 refresh_token 刷新。在 Vue 3 中合理的方式是把令牌存储在 Pinia 或 localStorage,并在 Axios 拦截器中统一附加 Authorization 头。下面代码展示了通过 OAuth 密码模式获取令牌的过程,适用于自有实例或可信环境。
// 使用 axios 获取 Pixelfed OAuth 令牌
import axios from 'axios';
const instance = axios.create({
baseURL: '/pixelfed',
timeout: 10000,
});
async function getToken(username, password) {
const params = new URLSearchParams();
params.append('grant_type', 'password');
params.append('client_id', import.meta.env.VITE_PIXELFED_CLIENT_ID);
params.append('client_secret', import.meta.env.VITE_PIXELFED_CLIENT_SECRET);
params.append('username', username);
params.append('password', password);
const { data } = await instance.post('/oauth/token', params);
return data; // 包含 access_token, refresh_token, expires_in
}
Vue 3 中的请求封装与状态管理
拿到令牌后,不要在每个组件里手写 fetch,应当建立统一的 Axios 实例。这个实例需要做三件事:自动附加 access_token;遇到 401 时尝试刷新令牌并重试原请求;对网络错误和业务错误做统一提示。Vue 3 的 Composition API 让这些逻辑可以封装成独立的 usePixelfedClient 组合式函数,避免与组件耦合。
下面是一个 Axios 拦截器示例,它从 Pinia store 中读取令牌,如果当前没有令牌则不添加 Authorization。刷新令牌时使用单例 Promise 防止并发请求同时触发多次刷新。代码中的 getAccessToken 和 refreshAccessToken 是假设已经实现的辅助函数,实际项目可替换为本地存储或 Pinia 状态。
import axios from 'axios';
import { useAuthStore } from '@/stores/auth';
const http = axios.create({
baseURL: '/pixelfed',
timeout: 15000,
});
http.interceptors.request.use((config) => {
const auth = useAuthStore();
if (auth.accessToken) {
config.headers.Authorization = `Bearer ${auth.accessToken}`;
}
return config;
});
let refreshPromise = null;
http.interceptors.response.use(
(response) => response,
async (error) => {
const auth = useAuthStore();
const originalRequest = error.config;
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
if (!refreshPromise) {
refreshPromise = auth.refreshToken().finally(() => {
refreshPromise = null;
});
}
await refreshPromise;
originalRequest.headers.Authorization = `Bearer ${auth.accessToken}`;
return http(originalRequest);
}
return Promise.reject(error);
},
);
状态管理方面,Pinia 适合放置授权信息和时间线数据。一个典型的 auth store 包含 accessToken、refreshToken、instanceUrl,并提供登录、刷新、登出 actions。时间线 store 则维护帖子列表、分页游标、加载状态,避免组件内部出现复杂的数据同步逻辑。由于 Pixelfed 返回的分页数据通常用 Link 头或 max_id 参数,store 中需要记录 nextMaxId 以便加载更多。
图片上传、时间线与数据加载优化
Pixelfed 的上传接口是 /api/v1/media,要求 multipart/form-data 格式,文件字段名为 file,可以附带描述文本。图片上传后返回 media 对象,包含 id 和 url,之后再调用 /api/v1/statuses 创建状态,把 media_ids 作为参数。上传大图时建议使用 Axios 的 onUploadProgress 回调更新进度条,提升用户体验。注意 Pixelfed 对图片大小和类型有限制,常见允许 JPEG、PNG、WebP,上传前可在前端做预校验。
下面是一个上传并发布图片的 Composable 实现,它返回 uploading、progress、error 和 publish 方法。代码中使用了 FormData 构造请求体,并在调用 statuses 接口时传入 media_ids 数组。发布成功后可以清空表单或刷新时间线。
import { ref } from 'vue';
import http from '@/api/client';
export function useImagePublisher() {
const uploading = ref(false);
const progress = ref(0);
const error = ref(null);
async function uploadImage(file, description = '') {
const formData = new FormData();
formData.append('file', file);
formData.append('description', description);
uploading.value = true;
progress.value = 0;
error.value = null;
try {
const { data } = await http.post('/api/v1/media', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
onUploadProgress: (e) => {
if (e.total) {
progress.value = Math.round((e.loaded / e.total) * 100);
}
},
});
return data;
} catch (err) {
error.value = err;
throw err;
} finally {
uploading.value = false;
}
}
async function publish(file, description) {
const media = await uploadImage(file, description);
const { data } = await http.post('/api/v1/statuses', {
status: description,
media_ids: [media.id],
});
return data;
}
return { uploading, progress, error, publish };
}
时间线加载通常使用 /api/v1/timelines/home 或 /api/v1/timelines/public,返回按时间倒序的帖子列表。为了流畅体验,前端要实现无限滚动:当滚动到底部附近时,用 max_id 请求下一页,将旧数据与新数据合并。注意去重,因为联邦网络可能存在延迟重复。Vue 3 中可以使用 IntersectionObserver 监听一个底部哨兵元素,触发加载函数,或者用第三方库如 @vueuse/core 的 useIntersectionObserver。结合 Pinia,维护 items、nextMaxId、isLoading,并提供 loadMore action,组件只负责调用。
缓存层面,Pixelfed 实例返回的图片 URL 通常指向远程 CDN,前端可以用浏览器缓存配合 service worker 做离线体验,但不是必须。更实际的是对 API 响应做轻量缓存,比如用内存 Map 缓存最近的时间线数据,减少重复请求。切换路由时保留滚动位置,返回时恢复,避免用户每次重新加载整个列表。
构建部署与常见问题排查
Vue 3 项目构建后部署到生产环境,代理策略会改变。如果使用 Nginx,需要在 server 块中配置 location /pixelfed/ 反向代理到目标实例,同时处理 WebSocket(Pixelfed 实时通知可能用到)。代理时要设置 Host 头,避免部分实例根据 Host 判断请求来源。另外,如果前端部署在子路径,需要设置 Vite 的 base 选项,确保资源路径正确。
常见问题包括:上传图片后返回 422,可能是文件类型或大小不符;时间线请求 401,可能是令牌过期且刷新失败,此时应清除本地状态并跳转登录页;跨域错误在开发环境大多是代理未生效,检查 Vite 配置的 rewrite 规则。另一个容易忽略的点是 Pixelfed 实例的速率限制,频繁请求可能触发 429,建议在 Axios 拦截器中增加简单的退避重试。
去中心化图片分享的前端集成,本质是把一个联邦服务的 OpenAPI 稳定地封装到组件层。上述方案已经在多个 Vue 3 项目中验证,你可以根据具体实例调整端点前缀和权限范围。只要把令牌刷新、上传进度、分页加载这些核心逻辑隔离在独立模块中,后续维护成本会大幅降低。